Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ LUME_RUNNER_ENV_FILE=~/Library/Application Support/github-runner-fleet/lume/runn
LUME_RUNNER_IPSW_PATH=~/Library/Application Support/github-runner-fleet/lume/cache/latest.ipsw
COMPOSE_PROJECT_NAME=github-runner-fleet
RUNNER_VERSION=2.333.0
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com:4318
OTEL_EXPORTER_OTLP_HEADERS=
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@ The Synology shell-only class supports shell jobs, JavaScript actions, composite
- [docker/runner-entrypoint.sh](docker/runner-entrypoint.sh): ephemeral registration and cleanup flow
- [src/cli.ts](src/cli.ts): config validation, compose rendering, and runner release helpers

## Runner Telemetry

Each runner pool can opt into OpenTelemetry by adding a `telemetry` block. When enabled, rendered Compose services and Lume runner exports include standard `OTEL_*` variables for the runner process. The collector endpoint stays configurable through deployment env interpolation, so the same non-secret pool config can point to different remote targets per host or environment.

```yaml
telemetry:
enabled: true
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}
protocol: http/protobuf
headers: ${OTEL_EXPORTER_OTLP_HEADERS:-}
tracesExporter: otlp
metricsExporter: otlp
logsExporter: otlp
resourceAttributes:
service.namespace: github-runner-fleet
```

Supported options are `endpoint`, `protocol` (`grpc` or `http/protobuf`), `headers`, `serviceName`, `tracesExporter`, `metricsExporter`, `logsExporter`, and `resourceAttributes`. Keep auth-bearing headers in `.env` or the target host environment, not in committed config. If `telemetry.enabled` is `true`, validation requires `endpoint`.

## Synology Quick Start

1. Copy `.env.example` to `.env` and set `GITHUB_PAT`.
Expand Down
7 changes: 7 additions & 0 deletions config/linux-docker-runners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ pools:
size: 2
architecture: amd64
runnerRoot: ${LINUX_DOCKER_RUNNER_BASE_DIR}/pools/linux-docker-private
# telemetry:
# enabled: true
# endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}
# protocol: http/protobuf
# headers: ${OTEL_EXPORTER_OTLP_HEADERS:-}
# resourceAttributes:
# service.namespace: github-runner-fleet
resources:
cpus: "4"
memory: 8g
7 changes: 7 additions & 0 deletions config/lume-runners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ pool:
guestRunnerRoot: /Users/lume/actions-runner
guestWorkRoot: /Users/lume/actions-runner/_work
runnerVersion: 2.333.0
# telemetry:
# enabled: true
# endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}
# protocol: http/protobuf
# headers: ${OTEL_EXPORTER_OTLP_HEADERS:-}
# resourceAttributes:
# service.namespace: github-runner-fleet
7 changes: 7 additions & 0 deletions config/pools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ pools:
cooldownSeconds: 120
architecture: auto
runnerRoot: ${SYNOLOGY_RUNNER_BASE_DIR}/pools/synology-private
# telemetry:
# enabled: true
# endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}
# protocol: http/protobuf
# headers: ${OTEL_EXPORTER_OTLP_HEADERS:-}
# resourceAttributes:
# service.namespace: github-runner-fleet
resources:
memory: 2g
- key: synology-public
Expand Down
7 changes: 7 additions & 0 deletions config/windows-runners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ pools:
sshPort: ${WINDOWS_DOCKER_PORT:-22}
image: ghcr.io/example/github-runner-fleet:0.1.9-windows
runnerRoot: ${WINDOWS_DOCKER_RUNNER_BASE_DIR}/pools/windows-private
# telemetry:
# enabled: true
# endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT}
# protocol: http/protobuf
# headers: ${OTEL_EXPORTER_OTLP_HEADERS:-}
# resourceAttributes:
# service.namespace: github-runner-fleet
labels:
- x64
resources:
Expand Down
19 changes: 17 additions & 2 deletions src/lib/compose.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import YAML from "yaml";
import type { DeploymentEnv } from "./env.js";
import type { PoolConfig, ResolvedConfig } from "./config.js";
import {
renderTelemetryEnvironment,
type PoolConfig,
type ResolvedConfig
} from "./config.js";

export function renderCompose(
config: ResolvedConfig,
Expand Down Expand Up @@ -60,7 +64,18 @@ function renderService(pool: PoolConfig, index: number): Record<string, unknown>
RUNNER_TOOL_CACHE: "/opt/hostedtoolcache",
AGENT_TOOLSDIRECTORY: "/opt/hostedtoolcache",
RUNNER_EPHEMERAL: "true",
RUNNER_DISABLE_UPDATE: "true"
RUNNER_DISABLE_UPDATE: "true",
...renderTelemetryEnvironment(pool.telemetry, {
serviceName: "github-runner-fleet.synology",
resourceAttributes: {
"deployment.environment": pool.visibility,
"github.organization": pool.organization,
"runner.group": pool.runnerGroup,
"runner.name": buildRunnerName(pool, index),
"runner.pool": pool.key,
"runner.plane": "synology"
}
})
};

if (pool.repositoryAccess === "selected") {
Expand Down
15 changes: 13 additions & 2 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import YAML from "yaml";
import { z } from "zod";
import type { DeploymentEnv } from "./env.js";
import type { RunnerArchitecture } from "./runner-version.js";
import {
renderTelemetryEnvironment,
telemetrySchema,
type TelemetryConfig
} from "./telemetry.js";

export type RunnerVisibility = "private" | "public";
export type RepositoryAccess = "all" | "selected";
Expand Down Expand Up @@ -35,6 +40,7 @@ export interface PoolConfig {
runnerRoot: string;
resources: PoolResources;
scaling?: PoolScaling;
telemetry?: TelemetryConfig;
imageRef: string;
}

Expand Down Expand Up @@ -97,7 +103,8 @@ const poolSchema = z
queueThreshold: z.number().int().min(1),
cooldownSeconds: z.number().int().min(0)
})
.optional()
.optional(),
telemetry: telemetrySchema
})
.superRefine((pool, ctx) => {
if (pool.repositoryAccess === "selected" && pool.allowedRepositories.length === 0) {
Expand Down Expand Up @@ -148,6 +155,7 @@ export function loadConfig(

const seenKeys = new Set<string>();
const pools = result.pools.map((pool) => {
const { telemetry, ...poolValues } = pool;
if (seenKeys.has(pool.key)) {
throw new Error(`duplicate pool key: ${pool.key}`);
}
Expand All @@ -171,14 +179,15 @@ export function loadConfig(
}

return {
...pool,
...poolValues,
labels: uniqueLabels(pool.labels, pool.visibility),
resources: {
cpus: pool.resources.cpus,
memory: pool.resources.memory,
pidsLimit: pool.resources.pidsLimit
},
scaling: pool.scaling,
...(telemetry.enabled ? { telemetry } : {}),
imageRef: `${result.image.repository}:${result.image.tag}`
};
});
Expand All @@ -190,6 +199,8 @@ export function loadConfig(
};
}

export { renderTelemetryEnvironment };

function uniqueLabels(
labels: string[],
visibility: RunnerVisibility
Expand Down
14 changes: 13 additions & 1 deletion src/lib/linux-docker-compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ResolvedLinuxDockerConfig
} from "./linux-docker-config.js";
import type { DeploymentEnv } from "./env.js";
import { renderTelemetryEnvironment } from "./telemetry.js";

export function renderLinuxDockerCompose(
config: ResolvedLinuxDockerConfig,
Expand Down Expand Up @@ -73,7 +74,18 @@ function renderService(
RUNNER_EXEC_MODE_OVERRIDE: "root",
RUNNER_EPHEMERAL: "true",
RUNNER_DISABLE_UPDATE: "true",
DOCKER_HOST: "unix:///var/run/docker.sock"
DOCKER_HOST: "unix:///var/run/docker.sock",
...renderTelemetryEnvironment(pool.telemetry, {
serviceName: "github-runner-fleet.linux-docker",
resourceAttributes: {
"deployment.environment": pool.visibility,
"github.organization": pool.organization,
"runner.group": pool.runnerGroup,
"runner.name": buildLinuxDockerServiceName(pool, index),
"runner.pool": pool.key,
"runner.plane": "linux-docker"
}
})
};

if (pool.repositoryAccess === "selected") {
Expand Down
9 changes: 7 additions & 2 deletions src/lib/linux-docker-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
RunnerPlatform
} from "./config.js";
import type { DeploymentEnv } from "./env.js";
import { telemetrySchema, type TelemetryConfig } from "./telemetry.js";

export interface LinuxDockerPoolConfig {
key: string;
Expand All @@ -21,6 +22,7 @@ export interface LinuxDockerPoolConfig {
architecture: RunnerPlatform;
runnerRoot: string;
resources: PoolResources;
telemetry?: TelemetryConfig;
imageRef: string;
}

Expand Down Expand Up @@ -54,7 +56,8 @@ const poolSchema = z
memory: z.string().min(1).optional(),
pidsLimit: z.number().int().positive().optional()
})
.default({})
.default({}),
telemetry: telemetrySchema
})
.superRefine((pool, ctx) => {
if (pool.repositoryAccess === "selected" && pool.allowedRepositories.length === 0) {
Expand Down Expand Up @@ -97,6 +100,7 @@ export function loadLinuxDockerConfig(

const seenKeys = new Set<string>();
const pools = result.pools.map((pool) => {
const { telemetry, ...poolValues } = pool;
if (seenKeys.has(pool.key)) {
throw new Error(`duplicate linux-docker pool key: ${pool.key}`);
}
Expand All @@ -120,14 +124,15 @@ export function loadLinuxDockerConfig(
}

return {
...pool,
...poolValues,
visibility: "private" as const,
labels: uniqueLabels(pool.labels),
resources: {
cpus: pool.resources.cpus,
memory: pool.resources.memory,
pidsLimit: pool.resources.pidsLimit
},
...(telemetry.enabled ? { telemetry } : {}),
imageRef: `${result.image.repository}:${result.image.tag}`
};
});
Expand Down
28 changes: 24 additions & 4 deletions src/lib/lume-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import path from "node:path";
import YAML from "yaml";
import { z } from "zod";
import type { DeploymentEnv } from "./env.js";
import {
renderTelemetryEnvironment,
telemetrySchema,
type TelemetryConfig
} from "./telemetry.js";

export interface LumePoolConfig {
key: string;
Expand All @@ -23,6 +28,7 @@ export interface LumePoolConfig {
guestRunnerRoot: string;
guestWorkRoot: string;
runnerVersion: string;
telemetry?: TelemetryConfig;
}

export interface LumeSlotManifest {
Expand Down Expand Up @@ -73,7 +79,8 @@ const poolSchema = z.object({
guestPassword: z.string().min(1).default("lume"),
guestRunnerRoot: z.string().min(1).default("/Users/lume/actions-runner"),
guestWorkRoot: z.string().min(1).default("/Users/lume/actions-runner/_work"),
runnerVersion: z.string().min(1).optional()
runnerVersion: z.string().min(1).optional(),
telemetry: telemetrySchema
});

const configSchema = z.object({
Expand All @@ -99,11 +106,13 @@ export function loadLumeConfig(
throw new Error("LUME_RUNNER_ENV_FILE must resolve to an absolute path");
}

const normalizedLabels = normalizeLabels(result.pool.labels);
const { telemetry, ...poolValues } = result.pool;
const normalizedLabels = normalizeLabels(poolValues.labels);
const pool: LumePoolConfig = {
...result.pool,
...poolValues,
labels: normalizedLabels,
runnerVersion: result.pool.runnerVersion ?? env.runnerVersion
runnerVersion: poolValues.runnerVersion ?? env.runnerVersion,
...(telemetry.enabled ? { telemetry } : {})
};

const host = {
Expand Down Expand Up @@ -161,6 +170,17 @@ export function renderLumeShellExports(
LUME_SLOT_KEY: slot.slotKey,
LUME_VM_NAME: slot.vmName,
RUNNER_NAME: slot.runnerName,
...renderTelemetryEnvironment(config.pool.telemetry, {
serviceName: "github-runner-fleet.lume",
resourceAttributes: {
"github.organization": config.pool.organization,
"runner.group": config.pool.runnerGroup,
"runner.name": slot.runnerName,
"runner.pool": config.pool.key,
"runner.plane": "lume",
"runner.slot": slot.slotKey
}
}),
LUME_SLOT_DIR: slot.hostDir,
LUME_SLOT_WORKER_PID_FILE: slot.workerPidFile,
LUME_SLOT_VM_PID_FILE: slot.vmPidFile,
Expand Down
Loading