get.porter.sh/porter@v1.3.0/pkg/portercontext/telemetry.go (about)

     1  package portercontext
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"time"
    11  
    12  	"get.porter.sh/porter/pkg"
    13  	"get.porter.sh/porter/pkg/tracing"
    14  	"go.opentelemetry.io/otel"
    15  	"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
    16  	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    17  	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    18  	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
    19  	"go.opentelemetry.io/otel/propagation"
    20  	"go.opentelemetry.io/otel/sdk/resource"
    21  	sdktrace "go.opentelemetry.io/otel/sdk/trace"
    22  	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
    23  	"go.opentelemetry.io/otel/trace/noop"
    24  	"go.uber.org/zap"
    25  	"google.golang.org/grpc/credentials"
    26  )
    27  
    28  func (c *Context) configureTelemetry(ctx context.Context, cfg LogConfiguration, logger *zap.Logger) error {
    29  	if cfg.TelemetryServiceName == "" {
    30  		cfg.TelemetryServiceName = "porter"
    31  	}
    32  
    33  	c.tracer = createNoopTracer()
    34  
    35  	tracer, err := c.createTracer(ctx, cfg, logger)
    36  	if err != nil {
    37  		return err
    38  	}
    39  
    40  	// Only assign the tracer if one was configured (i.e. not noop)
    41  	if !tracer.IsNoOp {
    42  		c.tracer = tracer
    43  		c.tracerInitalized = true
    44  	}
    45  	return nil
    46  }
    47  
    48  func createNoopTracer() tracing.Tracer {
    49  	tracer := noop.NewTracerProvider().Tracer("noop")
    50  	cleanup := func(_ context.Context) error { return nil }
    51  	t := tracing.NewTracer(tracer, cleanup)
    52  	t.IsNoOp = true
    53  	return t
    54  }
    55  
    56  func (c *Context) createTracer(ctx context.Context, cfg LogConfiguration, logger *zap.Logger) (tracing.Tracer, error) {
    57  	client, err := c.createTraceClient(c.logCfg)
    58  	if err != nil {
    59  		return tracing.Tracer{}, err
    60  	}
    61  	if client == nil {
    62  		logger.Debug("telemetry disabled")
    63  		return createNoopTracer(), nil
    64  	}
    65  
    66  	var exporter sdktrace.SpanExporter
    67  	if cfg.TelemetryRedirectToFile {
    68  		// Instead of sending trace data to a collector
    69  		// save them to a file so that we can test our traces
    70  		testTracePath := filepath.Join(cfg.TelemetryDirectory, c.buildLogFileName())
    71  		logger.Debug(fmt.Sprintf("redirecting open telemetry trace data to a file: %s", testTracePath))
    72  
    73  		tracesDir := filepath.Dir(testTracePath)
    74  		if err = c.FileSystem.MkdirAll(tracesDir, pkg.FileModeDirectory); err != nil {
    75  			return tracing.Tracer{}, fmt.Errorf("could not create traces directory at  %s: %w", tracesDir, err)
    76  		}
    77  
    78  		f, err := c.FileSystem.OpenFile(testTracePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, pkg.FileModeWritable)
    79  		if err != nil {
    80  			return tracing.Tracer{}, fmt.Errorf("could not create test traces file at  %s: %w", testTracePath, err)
    81  		}
    82  		c.traceFile = f
    83  		exporter, err = stdouttrace.New(stdouttrace.WithWriter(f))
    84  		if err != nil {
    85  			return tracing.Tracer{}, fmt.Errorf("error creating a file trace exporter: %w", err)
    86  		}
    87  	} else {
    88  		createTraceCtx, cancel := context.WithTimeout(ctx, cfg.TelemetryStartTimeout)
    89  		defer cancel()
    90  		exporter, err = otlptrace.New(createTraceCtx, client)
    91  		if err != nil {
    92  			return tracing.Tracer{}, fmt.Errorf("error creating an open telemetry trace exporter: %w", err)
    93  		}
    94  	}
    95  
    96  	serviceVersion := pkg.Version
    97  	if serviceVersion == "" {
    98  		serviceVersion = "dev"
    99  	}
   100  	r := resource.NewWithAttributes(
   101  		semconv.SchemaURL,
   102  		semconv.ServiceNameKey.String(cfg.TelemetryServiceName),
   103  		semconv.ServiceVersionKey.String(serviceVersion),
   104  	)
   105  
   106  	provider := sdktrace.NewTracerProvider(
   107  		sdktrace.WithBatcher(exporter),
   108  		sdktrace.WithResource(r),
   109  	)
   110  	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
   111  
   112  	tracer := provider.Tracer("") // empty tracer name defaults to the underlying trace implementor
   113  	cleanup := func(ctx context.Context) error {
   114  		return provider.Shutdown(ctx)
   115  	}
   116  	return tracing.NewTracer(tracer, cleanup), nil
   117  }
   118  
   119  // createTraceClient from the Porter configuration
   120  // See https://github.com/open-telemetry/opentelemetry-go/tree/main/exporters/otlp/otlptrace
   121  func (c *Context) createTraceClient(cfg LogConfiguration) (otlptrace.Client, error) {
   122  	if !cfg.TelemetryEnabled {
   123  		return nil, nil
   124  	}
   125  
   126  	switch cfg.TelemetryProtocol {
   127  	case "grpc":
   128  		opts := []otlptracegrpc.Option{}
   129  		if cfg.TelemetryEndpoint != "" {
   130  			opts = append(opts, otlptracegrpc.WithEndpoint(cfg.TelemetryEndpoint))
   131  		}
   132  		if cfg.TelemetryInsecure {
   133  			opts = append(opts, otlptracegrpc.WithInsecure())
   134  		}
   135  		if cfg.TelemetryCertificate != "" {
   136  			creds, err := credentials.NewClientTLSFromFile(cfg.TelemetryCertificate, "")
   137  			if err != nil {
   138  				return nil, fmt.Errorf("invalid telemetry certificate in %s: %w", cfg.TelemetryCertificate, err)
   139  			}
   140  			opts = append(opts, otlptracegrpc.WithTLSCredentials(creds))
   141  		}
   142  		if cfg.TelemetryTimeout != "" {
   143  			timeout, err := time.ParseDuration(cfg.TelemetryTimeout)
   144  			if err != nil {
   145  				return nil, fmt.Errorf("invalid telemetry timeout %s: %w", cfg.TelemetryTimeout, err)
   146  			}
   147  			opts = append(opts, otlptracegrpc.WithTimeout(timeout))
   148  		}
   149  		if cfg.TelemetryCompression != "" {
   150  			opts = append(opts, otlptracegrpc.WithCompressor(cfg.TelemetryCompression))
   151  		}
   152  		if len(cfg.TelemetryHeaders) > 0 {
   153  			opts = append(opts, otlptracegrpc.WithHeaders(cfg.TelemetryHeaders))
   154  		}
   155  		return otlptracegrpc.NewClient(opts...), nil
   156  	case "http/protobuf", "":
   157  		var opts []otlptracehttp.Option
   158  		if cfg.TelemetryEndpoint != "" {
   159  			opts = append(opts, otlptracehttp.WithEndpoint(cfg.TelemetryEndpoint))
   160  		}
   161  		if cfg.TelemetryInsecure {
   162  			opts = append(opts, otlptracehttp.WithInsecure())
   163  		}
   164  		if cfg.TelemetryCertificate != "" {
   165  			certB, err := c.FileSystem.ReadFile(cfg.TelemetryCertificate)
   166  			if err != nil {
   167  				return nil, fmt.Errorf("invalid telemetry certificate in %s: %w", cfg.TelemetryCertificate, err)
   168  			}
   169  			cp := x509.NewCertPool()
   170  			if ok := cp.AppendCertsFromPEM(certB); !ok {
   171  				return nil, fmt.Errorf("could not use certificate in %s", cfg.TelemetryCertificate)
   172  			}
   173  			opts = append(opts, otlptracehttp.WithTLSClientConfig(&tls.Config{RootCAs: cp}))
   174  		}
   175  		if cfg.TelemetryTimeout != "" {
   176  			timeout, err := time.ParseDuration(cfg.TelemetryTimeout)
   177  			if err != nil {
   178  				return nil, fmt.Errorf("invalid telemetry timeout %s. Supported values are durations such as 30s or 1m: %w", cfg.TelemetryTimeout, err)
   179  			}
   180  			opts = append(opts, otlptracehttp.WithTimeout(timeout))
   181  		}
   182  		if cfg.TelemetryCompression != "" {
   183  			var compression otlptracehttp.Compression
   184  			switch cfg.TelemetryCompression {
   185  			case "gzip":
   186  				compression = otlptracehttp.GzipCompression
   187  			default:
   188  				compression = otlptracehttp.NoCompression
   189  			}
   190  			opts = append(opts, otlptracehttp.WithCompression(compression))
   191  		}
   192  		if len(cfg.TelemetryHeaders) > 0 {
   193  			opts = append(opts, otlptracehttp.WithHeaders(cfg.TelemetryHeaders))
   194  		}
   195  		return otlptracehttp.NewClient(opts...), nil
   196  	default:
   197  		return nil, fmt.Errorf("invalid OTEL_EXPORTER_OTLP_PROTOCOL value %s. Only grpc and http/protobuf are supported", cfg.TelemetryProtocol)
   198  	}
   199  }