feat: OpenTelemetry tracing (#1206)

Adds OpenTelemetry tracing, configured through the standard `OTEL_*` environment variables and off until an OTLP endpoint is set.

![image.png](https://gitea.com/attachments/c4b49cdf-a32d-403c-94e9-30cd06ee6477)

Each job becomes a trace with a span per step, so operators can see where CI time goes in their existing observability backend. Jobs receive only `TRACEPARENT`, so instrumented tools inside them join the trace without seeing the collector's credentials. Metrics stay on the Prometheus endpoint, which a collector can scrape. See [docs/telemetry.md](https://gitea.com/wmTJc9IK0Q/runner/src/branch/otel/docs/telemetry.md) for setup and security notes.

Also keeps `GITHUB_SHA` and `GITHUB_REF` fixed for a whole run when HEAD moves during `exec` runs and the act tests.

[image.png](https://gitea.com/attachments/c4b49cdf-a32d-403c-94e9-30cd06ee6477)

[image.png](https://gitea.com/attachments/c4b49cdf-a32d-403c-94e9-30cd06ee6477)

---------

Co-authored-by: silverwind <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-authored-by: bircni <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1206
Reviewed-by: bircni <[email protected]>
Reviewed-by: silverwind <[email protected]>
Co-authored-by: wmTJc9IK0Q <[email protected]>
This commit is contained in:
authored and silverwind committed 2026-09-24 15:57:18 +00:00
1 parent 7ad12038d1
commit 1952e5cdb6
15 files changed
+603 -28

No files matched your search

+13
View File
@@ -25,6 +25,7 @@ import (
"gitea.com/gitea/runner/internal/pkg/lock"
"gitea.com/gitea/runner/internal/pkg/metrics"
"gitea.com/gitea/runner/internal/pkg/report"
"gitea.com/gitea/runner/internal/pkg/telemetry"
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
@@ -71,6 +72,18 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
defer func() { _ = releaseLock() }()
}
shutdownTelemetry, err := telemetry.Setup(ctx, reg.UUID, reg.Name)
if err != nil {
log.WithError(err).Warn("OpenTelemetry export failed to start")
}
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := shutdownTelemetry(ctx); err != nil {
log.WithError(err).Warn("OpenTelemetry export failed to flush")
}
}()
lbls := resolveLabels(daemArgs.Labels, cfg.Runner.Labels, reg.Labels)
ls := labels.Labels{}
+6
View File
@@ -5,6 +5,7 @@ package run
import (
"bytes"
"cmp"
"context"
"encoding/json/v2"
"errors"
@@ -34,6 +35,7 @@ import (
"gitea.com/gitea/runner/internal/pkg/labels"
"gitea.com/gitea/runner/internal/pkg/metrics"
"gitea.com/gitea/runner/internal/pkg/report"
"gitea.com/gitea/runner/internal/pkg/telemetry"
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
@@ -298,6 +300,7 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
defer cancel()
ctx, endJob := telemetry.StartJob(ctx, task)
// A proxy URL may carry credentials, and every job is given it; keep them out of the log.
reporter := report.NewReporter(ctx, cancel, r.client, task, r.cfg, proxyPasswords()...)
var volumeCleanup []common.Executor
@@ -322,6 +325,7 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
metrics.JobDuration.Observe(time.Since(start).Seconds())
metrics.JobsTotal.WithLabelValues(metrics.ResultToStatusLabel(reporter.Result())).Inc()
endJob(reporter.Result())
}()
reporter.RunDaemon()
runErr = r.run(ctx, task, reporter)
@@ -409,6 +413,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
}
job := workflow.GetJob(jobID)
reporter.ResetSteps(len(job.Steps))
telemetry.SetJobName(ctx, cmp.Or(job.Name, jobID))
taskContext := task.Context.Fields
envs := r.cloneEnvs()
@@ -426,6 +431,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
if v := taskContext["run_id"].GetStringValue(); v != "" {
envs["GITEA_RUN_ID"] = v
}
telemetry.AddJobEnv(ctx, envs)
log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
r.getDefaultActionsURL(task),