feat(tracing): Start to implement tracing system
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Matthieu 'JP' DERASSE 2022-12-14 16:48:40 +00:00
parent 99bca634d2
commit 06f0722cdb
Signed by: mderasse
GPG Key ID: 55141C777B16A705
8 changed files with 354 additions and 5 deletions

9
go.mod
View File

@ -8,12 +8,17 @@ require (
github.com/juju/errors v1.0.0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/sirupsen/logrus v1.9.0
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
go.opentelemetry.io/otel v1.11.2
go.opentelemetry.io/otel/sdk v1.11.2
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
go.opentelemetry.io/otel/trace v1.11.2 // indirect
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
)

20
go.sum
View File

@ -5,6 +5,12 @@ 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/gemnasium/logrus-graylog-hook/v3 v3.1.0 h1:SLtCnpI5ZZaz4l7RSatEhppB1BBhUEu+DqGANJzJdEA=
github.com/gemnasium/logrus-graylog-hook/v3 v3.1.0/go.mod h1:wi1zWv9tIvyLSMLCAzgRP+YR24oLVQVBHfPPKjtht44=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg=
github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
@ -30,13 +36,21 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
go.opentelemetry.io/otel v1.11.2 h1:YBZcQlsVekzFsFbjygXMOXSs6pialIZxcjfO/mBDmR0=
go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI=
go.opentelemetry.io/otel/sdk v1.11.2 h1:GF4JoaEx7iihdMFu30sOyRx52HDHOkl9xQ8SMqNXUiU=
go.opentelemetry.io/otel/sdk v1.11.2/go.mod h1:wZ1WxImwpq+lVRo4vsmSOxdd+xwoUJ6rqyLc3SyX9aU=
go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0=
go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

2
tracing/README.md Normal file
View File

@ -0,0 +1,2 @@
# Tracing Common
Contain code that allow to interact with a tracing app such as jaeger / zipkin

167
tracing/config.go Normal file
View File

@ -0,0 +1,167 @@
package tracing
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/juju/errors"
"gopkg.in/yaml.v3"
"git.dev.m-and-m.ovh/mderasse/gocommon/convert"
"git.dev.m-and-m.ovh/mderasse/gocommon/tracing/exporter/jaeger"
"git.dev.m-and-m.ovh/mderasse/gocommon/tracing/exporter/zipkin"
)
// envPrefix is the prefix that will be used for any environnement variable used to configure the tracing system.
const envPrefix = "TRACING_"
// CONFIG_FILE is the default file that will be searched to apply configuration from the filesystem.
const defaultConfigFile = "tracing.yaml"
// SECRET_NAME is the default name of the secret that will be searched in vault.
const defaultSecretName = "tracing"
// ConfigStruct represent the configuration of our tracing system.
type ConfigStruct struct {
Attributes map[string]string `yaml:"attributes"`
Enabled bool `yaml:"enabled"`
Exporter ExporterName `yaml:"exporter"`
ServiceName string `yaml:"service_name"`
ServiceVersion string `yaml:"service_version"`
ServiceInstanceID string `yaml:"service_instance_id"`
JaegerConfig *jaeger.ConfigStruct `yaml:"jaeger_config"`
ZipkinConfig *zipkin.ConfigStruct `yaml:"zipkin_config"`
}
func newDefaultConfig() *ConfigStruct {
return &ConfigStruct{
Enabled: false,
}
}
func loadConfigFromVault(secret string) (*ConfigStruct, error) {
return nil, os.ErrNotExist
}
// loadConfigFromFile will read the given file and return a config struct.
func loadConfigFromFile(path string) (*ConfigStruct, error) {
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return nil, errors.Trace(err)
}
//nolint:gosec // we did compute the file path
f, err := os.ReadFile(path)
if err != nil {
return nil, errors.Trace(err)
}
var config ConfigStruct
if err := yaml.Unmarshal(f, &config); err != nil {
return nil, errors.Trace(err)
}
return &config, nil
}
func loadConfig() (*ConfigStruct, error) {
c, err := loadConfigFromVault(defaultSecretName)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
} else if err == nil {
return c, nil
}
c, err = loadConfigFromFile(defaultConfigFile)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
} else if err == nil {
return c, nil
}
return newDefaultConfig(), nil
}
// applyEnv will retrieve info from environment variable and overide those got by config / vault.
// validity of the value is not checked here and will be check in a IsValid method.
func (c *ConfigStruct) applyEnv() error {
if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "ENABLED")); v != "" {
b, err := strconv.ParseBool(v)
if err != nil {
return errors.NewNotValid(err, fmt.Sprintf("Invalid %s%s environment variable. Should be a boolean", envPrefix, "ENABLED"))
}
c.Enabled = b
}
if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "SERVICE_NAME")); v != "" {
c.ServiceName = v
}
if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "SERVICE_VERSION")); v != "" {
c.ServiceVersion = v
}
if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "SERVICE_INSTANCE_ID")); v != "" {
c.ServiceInstanceID = v
}
if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "EXPORTER")); v != "" {
c.Exporter = ExporterName(v)
}
// Attributes
if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "ATTRIBUTES")); v != "" {
attributeParts := strings.Split(v, ",")
for _, ap := range attributeParts {
attributeKV := strings.SplitN(ap, ":", 1)
if len(attributeKV) != 2 {
return errors.NotValidf(fmt.Sprintf("Invalid attribute %s in environment variable. Should be a key1:value1,key2:value2 format", ap))
}
c.Attributes[attributeKV[0]] = attributeKV[1]
}
}
return nil
}
// IsValid will check a config struct.
func (c *ConfigStruct) IsValid() error {
if c.Enabled {
if c.ServiceName == "" {
return errors.NotValidf("ServiceName is empty")
}
if !c.Exporter.IsValid() {
return errors.NotValidf("Invalid Exporter %s. Allowed values: %s", c.Exporter.String(), strings.Join(convert.StringerSliceToStringSlice(GetListExporterName()), ", "))
}
switch c.Exporter {
case ExporterName_JAEGER:
if c.JaegerConfig == nil {
return errors.NotValidf("jaeger configuration is empty")
}
err := c.JaegerConfig.IsValid()
if err != nil {
return errors.Trace(err)
}
case ExporterName_ZIPKIN:
if c.ZipkinConfig == nil {
return errors.NotValidf("zipkin configuration is empty")
}
err := c.ZipkinConfig.IsValid()
if err != nil {
return errors.Trace(err)
}
default:
return errors.NotValidf("Exporter not handled")
}
}
return nil
}

34
tracing/enum.go Normal file
View File

@ -0,0 +1,34 @@
package tracing
// Exporter Name Enum
// ExporterName is an exporter that will receive the tracing.
type ExporterName string
//nolint:exported // keeping the enum simple and readable.
const (
ExporterName_JAEGER ExporterName = "JAEGER"
ExporterName_ZIPKIN ExporterName = "ZIPKIN"
)
// IsValid check if the gaven ExporterName is part of the list of handled exporter name.
func (e ExporterName) IsValid() bool {
for _, v := range GetListExporterName() {
if e == v {
return true
}
}
return false
}
func (e ExporterName) String() string {
return string(e)
}
// GetListExporterName return a the list of possible ExporterName.
func GetListExporterName() []ExporterName {
return []ExporterName{
ExporterName_JAEGER,
ExporterName_ZIPKIN,
}
}

View File

@ -0,0 +1,23 @@
package jaeger
import (
"github.com/asaskevich/govalidator"
"github.com/juju/errors"
)
// ConfigStruct is the configuration for Jaeger Provider.
type ConfigStruct struct {
URL string `yaml:"url"`
}
// IsValid will check that the Jaeger configuration is valid.
func (c *ConfigStruct) IsValid() error {
if c.URL == "" {
return errors.NotValidf("URL is empty in Jaeger configuration")
} else if isValid := govalidator.IsURL(c.URL); !isValid {
return errors.NotValidf("URL is invalid in Jaeger configuration")
}
return nil
}

View File

@ -0,0 +1,23 @@
package zipkin
import (
"github.com/asaskevich/govalidator"
"github.com/juju/errors"
)
// ConfigStruct is the configuration for Zipkin Provider.
type ConfigStruct struct {
URL string `yaml:"url"`
}
// IsValid will check that the Zipkin configuration is valid.
func (c *ConfigStruct) IsValid() error {
if c.URL == "" {
return errors.NotValidf("URL is empty in Zipkin configuration")
} else if isValid := govalidator.IsURL(c.URL); !isValid {
return errors.NotValidf("URL is invalid in Zipkin configuration")
}
return nil
}

81
tracing/tracing.go Normal file
View File

@ -0,0 +1,81 @@
package tracing
import (
"github.com/juju/errors"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
)
// Init will try to initialize tracing by trying to retrieve config from multiple source.
func Init() (*sdktrace.TracerProvider, error) {
// loading configuration
c, err := loadConfig()
if err != nil {
return nil, errors.Trace(err)
}
return initFromSource(c)
}
// InitFromCustomVaultSecret will initialize tracing with a vault secret.
func InitFromCustomVaultSecret(secret string) (*sdktrace.TracerProvider, error) {
c, err := loadConfigFromVault(secret)
if err != nil {
return nil, errors.Trace(err)
}
return initFromSource(c)
}
// InitFromCustomFile will initialize tracing with a config file.
func InitFromCustomFile(path string) (*sdktrace.TracerProvider, error) {
c, err := loadConfigFromFile(path)
if err != nil {
return nil, errors.Trace(err)
}
return initFromSource(c)
}
func initFromSource(c *ConfigStruct) (*sdktrace.TracerProvider, error) {
err := c.applyEnv()
if err != nil {
return nil, errors.Trace(err)
}
return InitFromCustomConfig(c)
}
// InitFromCustomConfig will initialize tracing from a gaven config.
func InitFromCustomConfig(c *ConfigStruct) (*sdktrace.TracerProvider, error) {
err := c.IsValid()
if err != nil {
return nil, errors.Trace(err)
}
// Ensure default SDK resources and the required service name are set.
r, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("myService"),
semconv.ServiceVersionKey.String("1.0.0"),
semconv.ServiceInstanceIDKey.String("abcdef12345"),
),
)
if err != nil {
panic(err)
}
return sdktrace.NewTracerProvider(
sdktrace.WithResource(r),
), nil
}