feat(tracing): Start to implement tracing system
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
2
tracing/README.md
Normal file
2
tracing/README.md
Normal 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
167
tracing/config.go
Normal 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
34
tracing/enum.go
Normal 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,
|
||||
}
|
||||
}
|
23
tracing/exporter/jaeger/config.go
Normal file
23
tracing/exporter/jaeger/config.go
Normal 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
|
||||
}
|
23
tracing/exporter/zipkin/config.go
Normal file
23
tracing/exporter/zipkin/config.go
Normal 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
81
tracing/tracing.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user