Skip to content
This repository has been archived by the owner on Jun 14, 2023. It is now read-only.

feat: add gorm plugin #37

Merged
merged 3 commits into from
Nov 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/plugin_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
sql/test/sql_plugin_test.yaml: sql/**
kafkareporter/test/go_kafka_reporter_plugin_test.yaml: kafkareporter/**
dubbo-go/test/dubbo_go_plugin_test.yaml: dubbo-go/**
gorm/test/gorm_plugin_test.yaml: gorm/**

PluginsTest:
name: Plugin
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The plugins of [go2sky](https://github.com/SkyAPM/go2sky)
1. [go-kratos](kratos/README.md)
1. [sql](sql/README.md)
1. [dubbo-go](dubbo-go/README.md)
1. [gorm](gorm/README.md)

### Log Plugins

Expand Down
40 changes: 40 additions & 0 deletions gorm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Go2Sky with Gorm
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this doc should be linked from https://github.com/SkyAPM/go2sky-plugins#trace-plugins


## Installation

```bash
go get -u github.com/SkyAPM/go2sky-plugins/gorm
```

## Usage

```go
import (
gormPlugin "github.com/SkyAPM/go2sky-plugins/gorm"

"github.com/SkyAPM/go2sky"
"github.com/SkyAPM/go2sky/reporter"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)

// init reporter
re, err := reporter.NewLogReporter()
defer re.Close()

// init tracer
tracer, err := go2sky.NewTracer("service-name", go2sky.WithReporter(re))
if err != nil {
log.Fatalf("init tracer error: %v", err)
}

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

if err != nil {
log.Fatalf("open db error: %v \n", err)
}
db.Use(gormPlugin.New(tracer, "127.0.0.1:3306", gormPlugin.MYSQL))

// use with context
dbWithCtx := db.WithContext(ctx)
```
17 changes: 17 additions & 0 deletions gorm/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Copyright 2021 SkyAPM org
//
// 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 gorm
20 changes: 20 additions & 0 deletions gorm/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module github.com/SkyAPM/go2sky-plugins/gorm

go 1.16

require (
github.com/SkyAPM/go2sky v1.2.1-0.20211120040242-09e1525ced66
gorm.io/gorm v1.22.3
skywalking.apache.org/repo/goapi v0.0.0-20211014145040-b215a7f7b270
)

require (
github.com/google/uuid v1.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20211112145013-271947fe86fd // indirect
google.golang.org/grpc v1.42.0 // indirect
gorm.io/driver/mysql v1.2.0
)
232 changes: 232 additions & 0 deletions gorm/go.sum

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions gorm/gorm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//
// Copyright 2021 SkyAPM org
//
// 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 gorm

import (
"fmt"
"strings"
"time"

"github.com/SkyAPM/go2sky"
"gorm.io/gorm"
agentv3 "skywalking.apache.org/repo/goapi/collect/language/agent/v3"
)

var (
_ gorm.Plugin = &SkyWalking{}
)

const spanKey = "spanKey"

type SkyWalking struct {
tracer *go2sky.Tracer
opts *options
}

func New(tracer *go2sky.Tracer, opts ...Option) *SkyWalking {
options := &options{
dbType: UNKNOWN,
componentID: componentIDUnknown,
peer: "unknown",
reportQuery: false,
reportParam: false,
}

for _, o := range opts {
o(options)
}

return &SkyWalking{
tracer: tracer,
opts: options,
}
}

func (s *SkyWalking) Name() string {
return "gorm:skyWalking"
}

func (s *SkyWalking) Initialize(db *gorm.DB) (err error) {
// before database operation
db.Callback().Create().Before("gorm:create").Register("sky_create_span", s.BeforeCallback("create"))
db.Callback().Query().Before("gorm:query").Register("sky_create_span", s.BeforeCallback("query"))
db.Callback().Update().Before("gorm:update").Register("sky_create_span", s.BeforeCallback("update"))
db.Callback().Delete().Before("gorm:delete").Register("sky_create_span", s.BeforeCallback("delete"))
db.Callback().Row().Before("gorm:row").Register("sky_create_span", s.BeforeCallback("row"))
db.Callback().Raw().Before("gorm:raw").Register("sky_create_span", s.BeforeCallback("raw"))

// after database operation
db.Callback().Create().After("gorm:create").Register("sky_end_span", s.AfterCallback())
db.Callback().Query().After("gorm:query").Register("sky_end_span", s.AfterCallback())
db.Callback().Update().After("gorm:update").Register("sky_end_span", s.AfterCallback())
db.Callback().Delete().After("gorm:delete").Register("sky_end_span", s.AfterCallback())
db.Callback().Row().After("gorm:row").Register("sky_end_span", s.AfterCallback())
db.Callback().Raw().After("gorm:raw").Register("sky_end_span", s.AfterCallback())

return
}

func (s *SkyWalking) BeforeCallback(operation string) func(db *gorm.DB) {
tracer := s.tracer
peer := s.opts.peer

if tracer == nil {
return func(db *gorm.DB) {}
}

return func(db *gorm.DB) {
tableName := db.Statement.Table
operation := fmt.Sprintf("%s/%s", tableName, operation)

span, err := tracer.CreateExitSpan(db.Statement.Context, operation, peer, func(key, value string) error {
return nil
})
if err != nil {
db.Logger.Error(db.Statement.Context, "gorm:skyWalking failed to create exit span, got error: %v", err)
return
}

// set span from db instance's context to pass span
db.Set(spanKey, span)
}
}

func (s *SkyWalking) AfterCallback() func(db *gorm.DB) {
tracer := s.tracer
if tracer == nil {
return func(db *gorm.DB) {}
}

return func(db *gorm.DB) {
// get span from db instance's context
spanInterface, _ := db.Get(spanKey)
span, ok := spanInterface.(go2sky.Span)
if !ok {
return
}

defer span.End()

sql := db.Statement.SQL.String()
vars := db.Statement.Vars
err := db.Statement.Error

span.SetComponent(s.opts.componentID)
span.SetSpanLayer(agentv3.SpanLayer_Database)
span.Tag(go2sky.TagDBType, string(s.opts.dbType))
span.Tag(go2sky.TagDBInstance, s.opts.peer)

if s.opts.reportQuery {
span.Tag(go2sky.TagDBStatement, sql)
}
if s.opts.reportParam && len(vars) != 0 {
span.Tag(go2sky.TagDBSqlParameters, argsToString(vars))
}

if err != nil {
span.Error(time.Now(), err.Error())
}
}
}

func argsToString(args []interface{}) string {
sb := strings.Builder{}

switch len(args) {
case 0:
return ""
case 1:
return fmt.Sprintf("%v", args[0])
}

sb.WriteString(fmt.Sprintf("%v", args[0]))
for _, arg := range args[1:] {
sb.WriteString(fmt.Sprintf(", %v", arg))
}
return sb.String()
}
79 changes: 79 additions & 0 deletions gorm/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// Copyright 2021 SkyAPM org
//
// 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 gorm

type DBType string

const (
UNKNOWN DBType = "unknown"
MYSQL DBType = "mysql"
)

const (
componentIDUnknown = 0
componentIDMysql = 5012
)

type Option func(*options)

type options struct {
dbType DBType
peer string
componentID int32

reportQuery bool
reportParam bool
}

// WithSqlDBType set dbType option,
// dbType is used for parsing dsn string to peer address
// and setting componentID, if DB type is not support in DBType
// list, please use WithPeerAddr to set peer address manually
func WithSqlDBType(t DBType) Option {
return func(o *options) {
o.dbType = t
o.setComponentID()
}
}

// WithPeerAddr set the peer address to report
func WithPeerAddr(addr string) Option {
return func(o *options) {
o.peer = addr
}
}

func WithQueryReport() Option {
return func(o *options) {
o.reportQuery = true
}
}

func WithParamReport() Option {
return func(o *options) {
o.reportParam = true
}
}

func (o *options) setComponentID() {
switch o.dbType {
case MYSQL:
o.componentID = componentIDMysql
default:
o.componentID = componentIDUnknown
}
}
26 changes: 26 additions & 0 deletions gorm/test/Dockerfile.client
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
# Copyright 2021 SkyAPM org
#
# 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.
#

FROM golang:1.16

ADD . /go2sky
WORKDIR /go2sky

EXPOSE 8080

ENTRYPOINT ["go"]

CMD ["run", "test/client.go"]
Loading