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 }