Skip to content

Commit

Permalink
Added support for go-sql-driver (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
Thiyagu55 authored Sep 30, 2022
1 parent 69039b8 commit 89887ed
Show file tree
Hide file tree
Showing 7 changed files with 440 additions and 0 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/go-sql-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: go-sql package test

on:
push:
branches:
- master
paths:
- go/go-sql/**
pull_request:
paths:
- go/go-sql/**

jobs:
unittests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19

- name: Build
run: go build -v ./...
working-directory: ./go/go-sql

- name: Test
run: go test -v ./...
working-directory: ./go/go-sql

gofmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19

# Verify go fmt standards are used
- name: Format
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi

2 changes: 2 additions & 0 deletions .github/workflows/unit-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ on:
- php/sqlcommenter-php/samples/sqlcommenter-laravel/**
- python/sqlcommenter-python/**
- nodejs/**
- go/**
pull_request:
paths-ignore:
- php/sqlcommenter-php/packages/sqlcommenter-laravel/**
- php/sqlcommenter-php/samples/sqlcommenter-laravel/**
- python/sqlcommenter-python/**
- nodejs/**
- go/**

jobs:
unittests:
Expand Down
72 changes: 72 additions & 0 deletions go/go-sql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Sqlcommenter [In development]

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.

## Installation

### Install from source

* Clone the source
* In terminal go inside the client folder location where we need to import google-sqlcommenter package and enter the below commands

```shell
go mod edit -replace google.com/sqlcommenter=path/to/google/sqlcommenter/go

go mod tiny
```
### Install from github [To be added]

## Usages

### go-sql-driver
Please use the sqlcommenter's default database driver to execute statements. \
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`.

```go
db, err := sqlcommenter.Open("<driver>", "<connectionString>", sqlcommenter.CommenterOptions{<tag>:<bool>})
```

#### Configuration

Users are given control over what tags they want to append by using `sqlcommenter.CommenterOptions` struct.

```go
type CommenterOptions struct {
EnableDBDriver bool
EnableTraceparent bool // OpenTelemetry trace information
EnableRoute bool // applicable for web frameworks
EnableFramework bool // applicable for web frameworks
EnableController bool // applicable for web frameworks
EnableAction bool // applicable for web frameworks
}
```

### net/http
Populate the request context with sqlcommenter.AddHttpRouterTags(r) function in a custom middleware.

#### Note
* We only support the `database/sql` driver and have provided an implementation for that.
* <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>

#### Example
```go
// middleware is used to intercept incoming HTTP calls and populate request context with commenter tags.
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := sqlcommenter.AddHttpRouterTags(r, next)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```

## Options

With Go SqlCommenter, we have configuration to choose which tags to be appended to the comment.

| Options | Included by default? | go-sql-orm | net/http | Notes |
| --------------- | :------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---: |
| `DBDriver` | | [ go-sql-driver](https://pkg.go.dev/database/sql/driver) | |
| `Action` | | | [net/http handle](https://pkg.go.dev/net/http#Handle) | |
| `Route` | | | [net/http routing path](https://pkg.go.dev/github.com/gorilla/mux#Route.URLPath) | |
| `Framework` | | | [net/http](https://pkg.go.dev/net/http) | |
| `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) | |
190 changes: 190 additions & 0 deletions go/go-sql/go-sql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2022 Google LLC
//
// 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 gosql

import (
"context"
"database/sql"
"fmt"
"net/http"
"net/url"
"reflect"
"runtime"
"sort"
"strings"

"go.opentelemetry.io/otel/propagation"
)

const (
route string = "route"
controller string = "controller"
action string = "action"
framework string = "framework"
driver string = "driver"
traceparent string = "traceparent"
)

type DB struct {
*sql.DB
options CommenterOptions
}

type CommenterOptions struct {
EnableDBDriver bool
EnableRoute bool
EnableFramework bool
EnableController bool
EnableAction bool
EnableTraceparent bool
}

func Open(driverName string, dataSourceName string, options CommenterOptions) (*DB, error) {
db, err := sql.Open(driverName, dataSourceName)
return &DB{DB: db, options: options}, err
}

// ***** Query Functions *****

func (db *DB) Query(query string, args ...any) (*sql.Rows, error) {
return db.DB.Query(db.withComment(context.Background(), query), args...)
}

func (db *DB) QueryRow(query string, args ...interface{}) *sql.Row {
return db.DB.QueryRow(db.withComment(context.Background(), query), args...)
}

func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
return db.DB.QueryContext(ctx, db.withComment(ctx, query), args...)
}

func (db *DB) Exec(query string, args ...any) (sql.Result, error) {
return db.DB.Exec(db.withComment(context.Background(), query), args...)
}

func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
return db.DB.ExecContext(ctx, db.withComment(ctx, query), args...)
}

func (db *DB) Prepare(query string) (*sql.Stmt, error) {
return db.DB.Prepare(db.withComment(context.Background(), query))
}

func (db *DB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
return db.DB.PrepareContext(ctx, db.withComment(ctx, query))
}

// ***** Query Functions *****

// ***** Framework Functions *****

func AddHttpRouterTags(r *http.Request, next any) context.Context { // any type is set because we need to refrain from importing http-router package
ctx := context.Background()
ctx = context.WithValue(ctx, route, r.URL.Path)
ctx = context.WithValue(ctx, action, getFunctionName(next))
ctx = context.WithValue(ctx, framework, "net/http")
return ctx
}

// ***** Framework Functions *****

// ***** Commenter Functions *****

func (db *DB) withComment(ctx context.Context, query string) string {
var commentsMap = map[string]string{}
query = strings.TrimSpace(query)

// Sorted alphabetically
if db.options.EnableAction && (ctx.Value(action) != nil) {
commentsMap[action] = ctx.Value(action).(string)
}

// `driver` information should not be coming from framework.
// So, explicitly adding that here.
if db.options.EnableDBDriver {
commentsMap[driver] = "database/sql"
}

if db.options.EnableFramework && (ctx.Value(framework) != nil) {
commentsMap[framework] = ctx.Value(framework).(string)
}

if db.options.EnableRoute && (ctx.Value(route) != nil) {
commentsMap[route] = ctx.Value(route).(string)
}

if db.options.EnableTraceparent {
carrier := extractTraceparent(ctx)
if val, ok := carrier["traceparent"]; ok {
commentsMap[traceparent] = val
}
}

var commentsString string = ""
if len(commentsMap) > 0 { // Converts comments map to string and appends it to query
commentsString = fmt.Sprintf("/*%s*/", convertMapToComment(commentsMap))
}

// A semicolon at the end of the SQL statement means the query ends there.
// We need to insert the comment before that to be considered as part of the SQL statemtent.
if query[len(query)-1:] == ";" {
return fmt.Sprintf("%s%s;", strings.TrimSuffix(query, ";"), commentsString)
}
return fmt.Sprintf("%s%s", query, commentsString)
}

// ***** Commenter Functions *****

// ***** Util Functions *****

func encodeURL(k string) string {
return url.QueryEscape(string(k))
}

func getFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

func convertMapToComment(tags map[string]string) string {
var sb strings.Builder
i, sz := 0, len(tags)

//sort by keys
sortedKeys := make([]string, 0, len(tags))
for k := range tags {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)

for _, key := range sortedKeys {
if i == sz-1 {
sb.WriteString(fmt.Sprintf("%s=%v", encodeURL(key), encodeURL(tags[key])))
} else {
sb.WriteString(fmt.Sprintf("%s=%v,", encodeURL(key), encodeURL(tags[key])))
}
i++
}
return sb.String()
}

func extractTraceparent(ctx context.Context) propagation.MapCarrier {
// Serialize the context into carrier
propgator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})
carrier := propagation.MapCarrier{}
propgator.Inject(ctx, carrier)
return carrier
}

// ***** Util Functions *****
Loading

0 comments on commit 89887ed

Please sign in to comment.