diff --git a/go.mod b/go.mod index 522b4a8..a74bf70 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 51b16e7..efa4431 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/tracing/README.md b/tracing/README.md new file mode 100644 index 0000000..ba66f3a --- /dev/null +++ b/tracing/README.md @@ -0,0 +1,2 @@ +# Tracing Common +Contain code that allow to interact with a tracing app such as jaeger / zipkin \ No newline at end of file diff --git a/tracing/config.go b/tracing/config.go new file mode 100644 index 0000000..19de364 --- /dev/null +++ b/tracing/config.go @@ -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 +} diff --git a/tracing/enum.go b/tracing/enum.go new file mode 100644 index 0000000..0f24296 --- /dev/null +++ b/tracing/enum.go @@ -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, + } +} diff --git a/tracing/exporter/jaeger/config.go b/tracing/exporter/jaeger/config.go new file mode 100644 index 0000000..a4c1b16 --- /dev/null +++ b/tracing/exporter/jaeger/config.go @@ -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 +} diff --git a/tracing/exporter/zipkin/config.go b/tracing/exporter/zipkin/config.go new file mode 100644 index 0000000..ec10ac2 --- /dev/null +++ b/tracing/exporter/zipkin/config.go @@ -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 +} diff --git a/tracing/tracing.go b/tracing/tracing.go new file mode 100644 index 0000000..b125642 --- /dev/null +++ b/tracing/tracing.go @@ -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 +}