Skip to content

QuangTung97/otelwrap

Repository files navigation

OtelWrap: code generation tool for Go OpenTelemetry

otelwrap Coverage Status

What is OtelWrap?

OtelWrap is a tool that generates a decorator implementation of any interfaces that can be used for instrumentation with Go OpenTelemetry library. Inspired by https://github.com/matryer/moq

Supporting:

  • Any interface and any method with context.Context as the first parameter.
  • Detecting error return and set the span's error status accordingly.
  • Only tested using go generate.
  • Interface embedding.

Installing

Using conventional tools.go file for pinning version in go.mod / go.sum.

// +build tools

package tools

import (
    _ "github.com/QuangTung97/otelwrap"
)

And then download and install the binary with commands:

$ go mod tidy
$ go install github.com/QuangTung97/otelwrap

Usage

otelwrap [flags] -source-dir interface [interface2 interface3 ...]
    --out string (required)
        output file

Using go generate:

package example

import "context"

//go:generate otelwrap --out interface_wrappers.go . MyInterface

type MyInterface interface {
    Method1(ctx context.Context) error
    Method2(ctx context.Context, x int)
    Method3()
}

The run go generate ./... in your module. The generated file looks like:

// Code generated by otelwrap; DO NOT EDIT.
// github.com/QuangTung97/otelwrap

package example

import (
    "context"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

// MyInterfaceWrapper wraps OpenTelemetry's span
type MyInterfaceWrapper struct {
    MyInterface
    tracer trace.Tracer
    prefix string
}

// NewMyInterfaceWrapper creates a wrapper
func NewMyInterfaceWrapper(wrapped MyInterface, tracer trace.Tracer, prefix string) *MyInterfaceWrapper {
    return &MyInterfaceWrapper{
        MyInterface: wrapped,
        tracer:      tracer,
        prefix:      prefix,
    }
}

// Method1 ...
func (w *MyInterfaceWrapper) Method1(ctx context.Context) (err error) {
    ctx, span := w.tracer.Start(ctx, w.prefix+"Method1")
    defer span.End()

    err = w.MyInterface.Method1(ctx)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
    }
    return err
}

// Method2 ...
func (w *MyInterfaceWrapper) Method2(ctx context.Context, x int) {
    ctx, span := w.tracer.Start(ctx, w.prefix+"Method2")
    defer span.End()

    w.MyInterface.Method2(ctx, x)
}

To use the generated struct, simply wraps the original implementation. The generated code is very easy to read.

package example

import "go.opentelemetry.io/otel"

func InitMyInterface() MyInterface {
    original := NewMyInterfaceImpl()
    return NewMyInterfaceWrapper(original, otel.GetTracerProvider().Tracer("example"), "prefix")
}

Can also generate for interfaces in other packages:

package example

import "path/to/another"

var _ another.Interface1 // not necessary, only for keeping the import statement

//go:generate otelwrap --out interface_wrappers.go . another.Interface1 another.Interface2

Or generate to another package:

package example

import "context"

//go:generate otelwrap --out ../another/interface_wrappers.go . MyInterface

type MyInterface interface {
    Method1(ctx context.Context) error
    Method2(ctx context.Context, x int)
    Method3()
}