diff --git a/clientutil/transport.go b/clientutil/transport.go
index 843a760b1e25eb1c21b2c8552b9c6ad107ea6d34..1cc6c6d34594869c8be446fd325c27d1ad2c3289 100644
--- a/clientutil/transport.go
+++ b/clientutil/transport.go
@@ -7,6 +7,8 @@ import (
 	"net/http"
 	"sync"
 	"time"
+
+	"git.autistici.org/ai3/go-common/tracing"
 )
 
 // The transportCache is just a cache of http transports, each
@@ -29,12 +31,12 @@ func newTransportCache(tlsConfig *tls.Config) *transportCache {
 }
 
 func (m *transportCache) newTransport(addr string) http.RoundTripper {
-	return &http.Transport{
+	return tracing.WrapTransport(&http.Transport{
 		TLSClientConfig: m.tlsConfig,
 		DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
 			return netDialContext(ctx, network, addr)
 		},
-	}
+	})
 }
 
 func (m *transportCache) getTransport(addr string) http.RoundTripper {
diff --git a/serverutil/http.go b/serverutil/http.go
index 09cc9bb39440f01b502cf375d9a3ec28777e4955..604ca98f54357b6d423f341f8e71cd589697555a 100644
--- a/serverutil/http.go
+++ b/serverutil/http.go
@@ -3,16 +3,18 @@ package serverutil
 import (
 	"context"
 	"crypto/tls"
+	"fmt"
 	"io"
 	"log"
 	"net"
 	"net/http"
-	"net/http/pprof"
+	_ "net/http/pprof"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
+	"git.autistici.org/ai3/go-common/tracing"
 	"github.com/coreos/go-systemd/daemon"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -77,6 +79,10 @@ func (config *ServerConfig) buildHTTPServer(h http.Handler) (*http.Server, error
 // the listener, otherwise it will handle graceful termination on
 // SIGINT or SIGTERM and return nil.
 func Serve(h http.Handler, config *ServerConfig, addr string) error {
+	// Wrap with tracing handler (exclude metrics and other
+	// debugging endpoints).
+	h = tracing.WrapHandler(h, guessEndpointName(addr))
+
 	// Create the HTTP server.
 	srv, err := config.buildHTTPServer(h)
 	if err != nil {
@@ -139,8 +145,10 @@ func defaultHandler(h http.Handler) http.Handler {
 	// Add an endpoint to serve Prometheus metrics.
 	root.Handle("/metrics", promhttp.Handler())
 
-	// Add the net/http/pprof debug handlers.
-	root.Handle("/debug/pprof/", pprof.Handler(""))
+	// Let the default net/http handler deal with /debug/
+	// URLs. Packages such as net/http/pprof register their
+	// handlers there in ways that aren't reproducible.
+	root.Handle("/debug/", http.DefaultServeMux)
 
 	// Forward everything else to the main handler, adding
 	// Prometheus instrumentation (requests to /metrics and
@@ -151,6 +159,18 @@ func defaultHandler(h http.Handler) http.Handler {
 	return root
 }
 
+func guessEndpointName(addr string) string {
+	_, port, err := net.SplitHostPort(addr)
+	if err != nil {
+		return addr
+	}
+	host, err := os.Hostname()
+	if err != nil {
+		return addr
+	}
+	return fmt.Sprintf("%s:%s", host, port)
+}
+
 // HTTP-related metrics.
 var (
 	// Since we instrument the root HTTP handler, we don't really
diff --git a/tracing/tracing.go b/tracing/tracing.go
new file mode 100644
index 0000000000000000000000000000000000000000..df6144b7dd054b4dfde6640d95351f872050275a
--- /dev/null
+++ b/tracing/tracing.go
@@ -0,0 +1,130 @@
+package tracing
+
+import (
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"path/filepath"
+	"sync"
+
+	openzipkin "github.com/openzipkin/zipkin-go"
+	zipkinHTTP "github.com/openzipkin/zipkin-go/reporter/http"
+	"go.opencensus.io/exporter/zipkin"
+	"go.opencensus.io/plugin/ochttp"
+	"go.opencensus.io/trace"
+)
+
+var (
+	// Enabled reports whether tracing is globally enabled or not.
+	Enabled bool
+
+	// The active tracing configuration, if Enabled is true.
+	config tracingConfig
+
+	initOnce sync.Once
+)
+
+const globalTracingConfigPath = "/etc/tracing/client.conf"
+
+type tracingConfig struct {
+	ReportURL string `json:"report_url"`
+}
+
+// Read the global tracing configuration file. Its location is
+// hardcoded, but it can be overriden using the TRACING_CONFIG
+// environment variable.
+func readTracingConfig() error {
+	// Read and decode configuration.
+	cfgPath := globalTracingConfigPath
+	if s := os.Getenv("TRACING_CONFIG"); s != "" {
+		cfgPath = s
+	}
+	data, err := ioutil.ReadFile(cfgPath)
+	if err != nil {
+		return err
+	}
+
+	if err := json.Unmarshal(data, &config); err != nil {
+		log.Printf("warning: error in tracing configuration: %v, tracing disabled", err)
+		return err
+	}
+
+	if config.ReportURL == "" {
+		log.Printf("warning: tracing configuration contains no report_url, tracing disabled")
+		return errors.New("no report_url")
+	}
+
+	return nil
+}
+
+// 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
+	}
+	return filepath.Base(os.Args[0])
+}
+
+// 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.
+func init() {
+	// Kill switch from environment.
+	if s := os.Getenv("TRACING_ENABLE"); s == "0" {
+		return
+	}
+
+	if err := readTracingConfig(); err != nil {
+		return
+	}
+
+	Enabled = true
+}
+
+func initTracing(endpointAddr string) {
+	initOnce.Do(func() {
+		localEndpoint, err := openzipkin.NewEndpoint(getServiceName(), endpointAddr)
+		if err != nil {
+			log.Printf("warning: error creating tracing endpoint: %v, tracing disabled", err)
+			return
+		}
+
+		reporter := zipkinHTTP.NewReporter(config.ReportURL)
+		ze := zipkin.NewExporter(reporter, localEndpoint)
+
+		trace.RegisterExporter(ze)
+		trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
+
+		log.Printf("tracing enabled (report_url %s)", config.ReportURL)
+
+		Enabled = true
+	})
+}
+
+// WrapTransport optionally wraps a http.RoundTripper with OpenCensus
+// tracing functionality, if it is globally enabled.
+func WrapTransport(t http.RoundTripper) http.RoundTripper {
+	if Enabled {
+		t = &ochttp.Transport{Base: t}
+	}
+	return t
+}
+
+// WrapHandler wraps a http.Handler with OpenCensus tracing
+// functionality, if globally enabled.
+func WrapHandler(h http.Handler, endpointAddr string) http.Handler {
+	if Enabled {
+		initTracing(endpointAddr)
+		h = &ochttp.Handler{Handler: h}
+	}
+	return h
+}