Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d2bba63
feat(config): add telemetry config block
LauJosefsen Jun 25, 2026
135cea9
feat(telemetry): add OTEL tracing core with OTLP/HTTP exporter
LauJosefsen Jun 25, 2026
c1bde12
fix(telemetry): add no-op/enabled shutdown tests and clarify header b…
LauJosefsen Jun 25, 2026
6c979eb
feat(cmd): start root telemetry span per invocation
LauJosefsen Jun 25, 2026
9af2dd8
feat(gitops): add per-project sync spans with git context
LauJosefsen Jun 25, 2026
4c2765d
feat(actions): add per-task action spans with exit code
LauJosefsen Jun 25, 2026
726f7df
docs: document telemetry configuration
LauJosefsen Jun 25, 2026
b9fdd24
style: fix import ordering in gitops telemetry test
LauJosefsen Jun 25, 2026
c047579
feat(telemetry): attach OS username and hostname to traces
LauJosefsen Jun 25, 2026
c7f8044
fix(telemetry): skip git exec when disabled, shorten flush, clarify r…
LauJosefsen Jun 25, 2026
ba559cf
fix(telemetry): address review comments
LauJosefsen Jun 25, 2026
5950e1b
feat(telemetry): add GITTE_TELEMETRY_DEBUG to surface export errors
LauJosefsen Jun 26, 2026
e33c1df
feat(telemetry): add OTEL logs provider and correlated log output han…
LauJosefsen Jun 26, 2026
3281615
fix(telemetry): resolve log emitter lazily and cover severity/hint ma…
LauJosefsen Jun 26, 2026
6d2d22a
feat(telemetry): add phase spans for startup/gitops/actions
LauJosefsen Jun 26, 2026
d1b64aa
feat(cmd): wrap gitte run phases in startup/gitops/actions spans
LauJosefsen Jun 26, 2026
fde5666
feat(startup): add per-check spans and ship check output as logs
LauJosefsen Jun 26, 2026
d12961c
feat(actions): add per-action spans, parent task spans, ship action o…
LauJosefsen Jun 26, 2026
40c22aa
fix(telemetry): ignore OnFinish for never-started (skipped) tasks in …
LauJosefsen Jun 26, 2026
8dd28b8
docs: document span hierarchy and OTEL log export
LauJosefsen Jun 26, 2026
0673b63
fix(telemetry): target /v1/logs for OTLP log export (was POSTing to s…
LauJosefsen Jun 26, 2026
c0d5b8d
feat(telemetry): include repo/task in gitops.sync and action.run span…
LauJosefsen Jun 26, 2026
e77aadd
feat(telemetry): label action task spans with enabled feature gates
LauJosefsen Jun 26, 2026
c470389
feat(telemetry): add gitte.env (injected env KEY=VALUE) to action tas…
LauJosefsen Jun 26, 2026
0f93c1f
fix(telemetry): record task errors on the action span, not just the t…
LauJosefsen Jun 29, 2026
0b98372
docs: clarify that injected env (incl. feature-gate env) is exported,…
LauJosefsen Jun 29, 2026
daf77ff
feat(telemetry): attach failed task's output tail as gitte.error_tail…
LauJosefsen Jun 29, 2026
0fa6e11
fix: gofmt + keep executor's cancellable ctx for task commands (stati…
LauJosefsen Jun 29, 2026
5122796
fix(telemetry): harden export lifecycle and log correlation
LauJosefsen Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Gitte keeps all your repos in sync, runs startup checks to verify your local mac
- [Commands](#commands)
- [Configuration](#configuration)
- [Environment variables](#environment-variables)
- [Telemetry](#telemetry)
- [Global flags](#global-flags)
- [State and override files](#state-and-override-files)

Expand Down Expand Up @@ -170,6 +171,58 @@ See [docs/config.md](./docs/config.md) for the full configuration reference.

---

## Telemetry

Gitte can export OpenTelemetry traces to an OTLP/HTTP endpoint (e.g. Elastic
APM) to help debug failures. Traces capture per-repo git context (branch,
commit SHA, dirty state) and per-task outcomes with errors. To identify which
developer and machine hit a failure, the OS username (`user.name`) and hostname
(`host.name`) are attached to every trace.

**What is recorded:** the gitte CLI arguments and each action's command line are
exported as span attributes (this is intentional — knowing what ran is the
point). Note that gitte's **injected** environment — a project's `env`,
`env_when`, and feature-gate env — is also exported on task spans (the
`gitte.env` attribute) and in the task logs; the inherited **process**
environment (everything in `os.Environ`) is **not**. So keep secrets out of
action command definitions, CLI arguments, and config `env`/feature-gate blocks
— pass real secrets through the process environment instead. Full remote URLs
are never collected either (repos are identified by name only).

Each `gitte run` produces a structured trace: a root span with child spans for
each phase (`startup`, `gitops`, `actions`), a span per startup check, a span
per action (e.g. `build`, `up`) parenting its task spans, and a span per repo
sync. Action and startup command output is also shipped as **OTEL logs**,
correlated to the span that produced each line (stdout → INFO, stderr → WARN).

Logs are enabled with tracing; set `GITTE_TELEMETRY_LOGS=off` to keep traces but
disable the (higher-volume) log export. Keep secrets out of command output —
log lines are exported verbatim.

Enable it via the shared config:

```yaml
telemetry:
endpoint: https://apm.example.com:8200
headers:
Authorization: "Bearer <secret-token>" # or: "ApiKey <base64-key>"
```

Environment variables:

| Variable | Effect |
|---|---|
| `GITTE_TELEMETRY=off` | Disable telemetry locally (kill-switch) |
| `GITTE_TELEMETRY_URL` | Override the endpoint |
| `GITTE_TELEMETRY_LOGS=off` | Disable OTEL log export (keep traces) |
| `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_HEADERS` | Standard OTEL env vars, honored as a fallback when no gitte endpoint is set |

Precedence: `GITTE_TELEMETRY=off` > `GITTE_TELEMETRY_URL` > config endpoint >
`OTEL_EXPORTER_OTLP_*`. Telemetry is best-effort and never blocks or slows
gitte; export failures are silently ignored.

---

## Global flags

```
Expand Down
69 changes: 69 additions & 0 deletions actions/features_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package actions

import (
"reflect"
"strings"
"testing"

"github.com/cego/gitte/config"
"github.com/cego/gitte/state"
)

func TestEnabledFeaturesForProject(t *testing.T) {
cfg := &config.GitteConfig{
FeatureGates: map[string]config.FeatureGate{
"feat-on": {}, // empty scope → applies to all projects
"feat-off": {}, // disabled in state
"feat-scoped-out": {Scope: config.FeatureScope{Projects: []string{"other"}}}, // enabled but scoped to a different project
},
}
st := &state.GitteState{Features: map[string]state.FeatureState{
"feat-on": {Enabled: true},
"feat-off": {Enabled: false},
"feat-scoped-out": {Enabled: true},
}}
proj := config.ProjectConfig{Remote: "git@github.com:example/myproj.git"}

got := enabledFeaturesForProject(cfg, st, "myproj", proj)
want := []string{"feat-on"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("enabledFeaturesForProject = %v, want %v", got, want)
}
}

func TestInjectedEnv(t *testing.T) {
cfg := &config.GitteConfig{
FeatureGates: map[string]config.FeatureGate{
"feat-on": {Effects: config.FeatureEffects{Env: map[string]string{"FEAT_VAR": "1"}}},
},
}
st := &state.GitteState{Features: map[string]state.FeatureState{"feat-on": {Enabled: true}}}
proj := config.ProjectConfig{
Remote: "git@github.com:example/myproj.git",
Env: map[string]string{"PROJ_VAR": "x"},
}
got := injectedEnv(cfg, st, "myproj", proj)
if got["PROJ_VAR"] != "x" || got["FEAT_VAR"] != "1" {
t.Fatalf("injectedEnv = %v, want PROJ_VAR=x and FEAT_VAR=1", got)
}
}

func TestOutputTail(t *testing.T) {
// stderr preferred when non-empty
if got := outputTail([]byte("the real error"), []byte("noise")); got != "the real error" {
t.Fatalf("stderr-preferred: got %q", got)
}
// falls back to stdout when stderr is blank (e.g. gitlab-ci-local prints to stdout)
if got := outputTail([]byte(" \n"), []byte("stdout failure")); got != "stdout failure" {
t.Fatalf("stdout-fallback: got %q", got)
}
// capped to the last errorTailBytes
big := []byte(strings.Repeat("x", errorTailBytes+500))
if got := outputTail(big, nil); len(got) != errorTailBytes {
t.Fatalf("cap: len=%d, want %d", len(got), errorTailBytes)
}
// empty when no output
if got := outputTail(nil, nil); got != "" {
t.Fatalf("empty: got %q", got)
}
}
Loading
Loading