Skip to content

Commit 89887ed

Browse files
authored
Added support for go-sql-driver (#161)
1 parent 69039b8 commit 89887ed

7 files changed

Lines changed: 440 additions & 0 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: go-sql package test
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
paths:
8+
- go/go-sql/**
9+
pull_request:
10+
paths:
11+
- go/go-sql/**
12+
13+
jobs:
14+
unittests:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v3
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v3
21+
with:
22+
go-version: 1.19
23+
24+
- name: Build
25+
run: go build -v ./...
26+
working-directory: ./go/go-sql
27+
28+
- name: Test
29+
run: go test -v ./...
30+
working-directory: ./go/go-sql
31+
32+
gofmt:
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v3
36+
37+
- name: Set up Go
38+
uses: actions/setup-go@v3
39+
with:
40+
go-version: 1.19
41+
42+
# Verify go fmt standards are used
43+
- name: Format
44+
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi
45+

.github/workflows/unit-tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ on:
99
- php/sqlcommenter-php/samples/sqlcommenter-laravel/**
1010
- python/sqlcommenter-python/**
1111
- nodejs/**
12+
- go/**
1213
pull_request:
1314
paths-ignore:
1415
- php/sqlcommenter-php/packages/sqlcommenter-laravel/**
1516
- php/sqlcommenter-php/samples/sqlcommenter-laravel/**
1617
- python/sqlcommenter-python/**
1718
- nodejs/**
19+
- go/**
1820

1921
jobs:
2022
unittests:

go/go-sql/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Sqlcommenter [In development]
2+
3+
SQLcommenter is a plugin/middleware/wrapper to augment application related information/tags with SQL Statements that can be used later to correlate user code with SQL statements.
4+
5+
## Installation
6+
7+
### Install from source
8+
9+
* Clone the source
10+
* In terminal go inside the client folder location where we need to import google-sqlcommenter package and enter the below commands
11+
12+
```shell
13+
go mod edit -replace google.com/sqlcommenter=path/to/google/sqlcommenter/go
14+
15+
go mod tiny
16+
```
17+
### Install from github [To be added]
18+
19+
## Usages
20+
21+
### go-sql-driver
22+
Please use the sqlcommenter's default database driver to execute statements. \
23+
Due to inherent nature of Go, the safer way to pass information from framework to database driver is via `context`. So, it is recommended to use the context based methods of `DB` interface like `QueryContext`, `ExecContext` and `PrepareContext`.
24+
25+
```go
26+
db, err := sqlcommenter.Open("<driver>", "<connectionString>", sqlcommenter.CommenterOptions{<tag>:<bool>})
27+
```
28+
29+
#### Configuration
30+
31+
Users are given control over what tags they want to append by using `sqlcommenter.CommenterOptions` struct.
32+
33+
```go
34+
type CommenterOptions struct {
35+
EnableDBDriver bool
36+
EnableTraceparent bool // OpenTelemetry trace information
37+
EnableRoute bool // applicable for web frameworks
38+
EnableFramework bool // applicable for web frameworks
39+
EnableController bool // applicable for web frameworks
40+
EnableAction bool // applicable for web frameworks
41+
}
42+
```
43+
44+
### net/http
45+
Populate the request context with sqlcommenter.AddHttpRouterTags(r) function in a custom middleware.
46+
47+
#### Note
48+
* We only support the `database/sql` driver and have provided an implementation for that.
49+
* <b>ORM related tags are added to the driver only when the tags are enabled in the commenter's driver's config and also the request context should passed to the querying functions</b>
50+
51+
#### Example
52+
```go
53+
// middleware is used to intercept incoming HTTP calls and populate request context with commenter tags.
54+
func middleware(next http.Handler) http.Handler {
55+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56+
ctx := sqlcommenter.AddHttpRouterTags(r, next)
57+
next.ServeHTTP(w, r.WithContext(ctx))
58+
})
59+
}
60+
```
61+
62+
## Options
63+
64+
With Go SqlCommenter, we have configuration to choose which tags to be appended to the comment.
65+
66+
| Options | Included by default? | go-sql-orm | net/http | Notes |
67+
| --------------- | :------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---: |
68+
| `DBDriver` | | [ go-sql-driver](https://pkg.go.dev/database/sql/driver) | |
69+
| `Action` | | | [net/http handle](https://pkg.go.dev/net/http#Handle) | |
70+
| `Route` | | | [net/http routing path](https://pkg.go.dev/github.com/gorilla/mux#Route.URLPath) | |
71+
| `Framework` | | | [net/http](https://pkg.go.dev/net/http) | |
72+
| `Opentelemetry` | | [W3C TraceContext.Traceparent](https://www.w3.org/TR/trace-context/#traceparent-field), [W3C TraceContext.Tracestate](https://www.w3.org/TR/trace-context/#tracestate-field) | [W3C TraceContext.Traceparent](https://www.w3.org/TR/trace-context/#traceparent-field), [W3C TraceContext.Tracestate](https://www.w3.org/TR/trace-context/#tracestate-field) | |

go/go-sql/go-sql.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package gosql
16+
17+
import (
18+
"context"
19+
"database/sql"
20+
"fmt"
21+
"net/http"
22+
"net/url"
23+
"reflect"
24+
"runtime"
25+
"sort"
26+
"strings"
27+
28+
"go.opentelemetry.io/otel/propagation"
29+
)
30+
31+
const (
32+
route string = "route"
33+
controller string = "controller"
34+
action string = "action"
35+
framework string = "framework"
36+
driver string = "driver"
37+
traceparent string = "traceparent"
38+
)
39+
40+
type DB struct {
41+
*sql.DB
42+
options CommenterOptions
43+
}
44+
45+
type CommenterOptions struct {
46+
EnableDBDriver bool
47+
EnableRoute bool
48+
EnableFramework bool
49+
EnableController bool
50+
EnableAction bool
51+
EnableTraceparent bool
52+
}
53+
54+
func Open(driverName string, dataSourceName string, options CommenterOptions) (*DB, error) {
55+
db, err := sql.Open(driverName, dataSourceName)
56+
return &DB{DB: db, options: options}, err
57+
}
58+
59+
// ***** Query Functions *****
60+
61+
func (db *DB) Query(query string, args ...any) (*sql.Rows, error) {
62+
return db.DB.Query(db.withComment(context.Background(), query), args...)
63+
}
64+
65+
func (db *DB) QueryRow(query string, args ...interface{}) *sql.Row {
66+
return db.DB.QueryRow(db.withComment(context.Background(), query), args...)
67+
}
68+
69+
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
70+
return db.DB.QueryContext(ctx, db.withComment(ctx, query), args...)
71+
}
72+
73+
func (db *DB) Exec(query string, args ...any) (sql.Result, error) {
74+
return db.DB.Exec(db.withComment(context.Background(), query), args...)
75+
}
76+
77+
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
78+
return db.DB.ExecContext(ctx, db.withComment(ctx, query), args...)
79+
}
80+
81+
func (db *DB) Prepare(query string) (*sql.Stmt, error) {
82+
return db.DB.Prepare(db.withComment(context.Background(), query))
83+
}
84+
85+
func (db *DB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
86+
return db.DB.PrepareContext(ctx, db.withComment(ctx, query))
87+
}
88+
89+
// ***** Query Functions *****
90+
91+
// ***** Framework Functions *****
92+
93+
func AddHttpRouterTags(r *http.Request, next any) context.Context { // any type is set because we need to refrain from importing http-router package
94+
ctx := context.Background()
95+
ctx = context.WithValue(ctx, route, r.URL.Path)
96+
ctx = context.WithValue(ctx, action, getFunctionName(next))
97+
ctx = context.WithValue(ctx, framework, "net/http")
98+
return ctx
99+
}
100+
101+
// ***** Framework Functions *****
102+
103+
// ***** Commenter Functions *****
104+
105+
func (db *DB) withComment(ctx context.Context, query string) string {
106+
var commentsMap = map[string]string{}
107+
query = strings.TrimSpace(query)
108+
109+
// Sorted alphabetically
110+
if db.options.EnableAction && (ctx.Value(action) != nil) {
111+
commentsMap[action] = ctx.Value(action).(string)
112+
}
113+
114+
// `driver` information should not be coming from framework.
115+
// So, explicitly adding that here.
116+
if db.options.EnableDBDriver {
117+
commentsMap[driver] = "database/sql"
118+
}
119+
120+
if db.options.EnableFramework && (ctx.Value(framework) != nil) {
121+
commentsMap[framework] = ctx.Value(framework).(string)
122+
}
123+
124+
if db.options.EnableRoute && (ctx.Value(route) != nil) {
125+
commentsMap[route] = ctx.Value(route).(string)
126+
}
127+
128+
if db.options.EnableTraceparent {
129+
carrier := extractTraceparent(ctx)
130+
if val, ok := carrier["traceparent"]; ok {
131+
commentsMap[traceparent] = val
132+
}
133+
}
134+
135+
var commentsString string = ""
136+
if len(commentsMap) > 0 { // Converts comments map to string and appends it to query
137+
commentsString = fmt.Sprintf("/*%s*/", convertMapToComment(commentsMap))
138+
}
139+
140+
// A semicolon at the end of the SQL statement means the query ends there.
141+
// We need to insert the comment before that to be considered as part of the SQL statemtent.
142+
if query[len(query)-1:] == ";" {
143+
return fmt.Sprintf("%s%s;", strings.TrimSuffix(query, ";"), commentsString)
144+
}
145+
return fmt.Sprintf("%s%s", query, commentsString)
146+
}
147+
148+
// ***** Commenter Functions *****
149+
150+
// ***** Util Functions *****
151+
152+
func encodeURL(k string) string {
153+
return url.QueryEscape(string(k))
154+
}
155+
156+
func getFunctionName(i interface{}) string {
157+
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
158+
}
159+
160+
func convertMapToComment(tags map[string]string) string {
161+
var sb strings.Builder
162+
i, sz := 0, len(tags)
163+
164+
//sort by keys
165+
sortedKeys := make([]string, 0, len(tags))
166+
for k := range tags {
167+
sortedKeys = append(sortedKeys, k)
168+
}
169+
sort.Strings(sortedKeys)
170+
171+
for _, key := range sortedKeys {
172+
if i == sz-1 {
173+
sb.WriteString(fmt.Sprintf("%s=%v", encodeURL(key), encodeURL(tags[key])))
174+
} else {
175+
sb.WriteString(fmt.Sprintf("%s=%v,", encodeURL(key), encodeURL(tags[key])))
176+
}
177+
i++
178+
}
179+
return sb.String()
180+
}
181+
182+
func extractTraceparent(ctx context.Context) propagation.MapCarrier {
183+
// Serialize the context into carrier
184+
propgator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})
185+
carrier := propagation.MapCarrier{}
186+
propgator.Inject(ctx, carrier)
187+
return carrier
188+
}
189+
190+
// ***** Util Functions *****

0 commit comments

Comments
 (0)