Skip to content
Snippets Groups Projects
tracing.go 4.7 KiB
Newer Older
ale's avatar
ale committed
package tracing

import (
	"encoding/json"
	"errors"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
ale's avatar
ale committed
	"sync"

	othttp "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	b3 "go.opentelemetry.io/contrib/propagators/b3"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/zipkin"
	"go.opentelemetry.io/otel/propagation"
ale's avatar
ale committed
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
ale's avatar
ale committed
	semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
ale's avatar
ale committed
)

var (
	// Enabled reports whether tracing is globally enabled or not.
	Enabled bool

	initOnce sync.Once
)

const globalTracingConfigPath = "/etc/tracing/client.conf"

type tracingConfig struct {
	ReportURL string `json:"report_url"`
	Sample    string `json:"sample"`
ale's avatar
ale committed
}

// Read the global tracing configuration file. Its location is
// hardcoded, but it can be overriden using the TRACING_CONFIG
// environment variable.
func readTracingConfig() (*tracingConfig, error) {
ale's avatar
ale committed
	// Read and decode configuration.
	cfgPath := globalTracingConfigPath
	if s := os.Getenv("TRACING_CONFIG"); s != "" {
		cfgPath = s
	}
	data, err := ioutil.ReadFile(cfgPath)
	if err != nil {
		return nil, err
	var config tracingConfig
ale's avatar
ale committed
	if err := json.Unmarshal(data, &config); err != nil {
		log.Printf("warning: error in tracing configuration: %v, tracing disabled", err)
		return nil, err
ale's avatar
ale committed
	}

	if config.ReportURL == "" {
		log.Printf("warning: tracing configuration contains no report_url, tracing disabled")
		return nil, errors.New("no report_url")
	return &config, nil
ale's avatar
ale committed
}

// Compute the service name for Zipkin: this is usually the program
// name (without path), but it can be overriden by the TRACING_SERVICE
// environment variable.
func getServiceName() string {
	if s := os.Getenv("TRACING_SERVICE"); s != "" {
		return s
	}
	if s, err := os.Executable(); err == nil {
		return filepath.Base(s)
	}
	return "unknown_service"
func defaultResource(serviceName string) *resource.Resource {
	hostname, _ := os.Hostname()
ale's avatar
ale committed
	r, _ := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String(serviceName),
			semconv.HostNameKey.String(hostname),
ale's avatar
ale committed
// Initialize tracing. Tracing will be enabled if the system-wide
// tracing configuration file is present and valid. Explicitly set
// TRACING_ENABLE=0 in the environment to disable tracing.
//
// We need to check the configuration as soon as possible, because
// it's likely that client transports are created before HTTP servers,
// and we need to wrap them with opencensus at creation time.
ale's avatar
ale committed
func initTracing(serviceName string) {
ale's avatar
ale committed
	initOnce.Do(func() {
		// Kill switch from environment.
		if s := os.Getenv("TRACING_ENABLE"); s == "0" {
			return
		}

		config, err := readTracingConfig()
		if err != nil {
			return
		}

		ze, err := zipkin.New(config.ReportURL)
ale's avatar
ale committed
		if err != nil {
			log.Printf("error creating Zipkin exporter: %v", err)
ale's avatar
ale committed
		// The sampling policy only applies to incoming requests for
		// which tracing is not already enabled: in this case, we
		// always pass-through.
		var sampler trace.Sampler
		switch config.Sample {
		case "", "always":
			sampler = trace.AlwaysSample()
		case "never":
			sampler = trace.NeverSample()
		default:
			frac, err := strconv.ParseFloat(config.Sample, 64)
			if err != nil {
				log.Printf("warning: error in tracing configuration: sample: %v, tracing disabled", err)
				return
			}
			sampler = trace.TraceIDRatioBased(frac)

		tp := trace.NewTracerProvider(
ale's avatar
ale committed
			trace.WithSampler(trace.ParentBased(sampler)),
			trace.WithBatcher(ze),
			trace.WithResource(defaultResource(serviceName)),
		)

		otel.SetTracerProvider(tp)
		otel.SetTextMapPropagator(
			propagation.NewCompositeTextMapPropagator(
				propagation.TraceContext{},
				propagation.Baggage{},
				b3.New()))

ale's avatar
ale committed
		log.Printf("tracing enabled (report_url %s)", config.ReportURL)

		Enabled = true
	})
}

// Init tracing support, if not using WrapHandler.
func Init() {
	initTracing(getServiceName())
ale's avatar
ale committed
// WrapTransport optionally wraps a http.RoundTripper with OpenCensus
// tracing functionality, if it is globally enabled.
//
// Must call Init() first.
ale's avatar
ale committed
func WrapTransport(t http.RoundTripper) http.RoundTripper {
	if Enabled {
ale's avatar
ale committed
		t = othttp.NewTransport(t)
ale's avatar
ale committed
	}
	return t
}

// WrapHandler wraps a http.Handler with OpenCensus tracing
// functionality, if globally enabled. Automatically calls Init().
ale's avatar
ale committed
func WrapHandler(h http.Handler, endpointAddr string) http.Handler {
	serviceName := getServiceName()
	initTracing(serviceName)
	if !Enabled {
		return h

	// Format span names with the request URL path.
ale's avatar
ale committed
	return othttp.NewHandler(
		h, serviceName,
		othttp.WithSpanNameFormatter(func(op string, r *http.Request) string {
			return r.URL.Path
		}),
	)