From dbe982df6d4e83f2daaaf61da2ded23804762f93 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 20 Oct 2026 20:26:55 -0400 Subject: [PATCH] fix(observability): meter checkpoint git commands so their dashboards stop reading empty Signed-off-by: Yordis Prieto --- apps/server/src/vcs/GitVcsDriver.test.ts | 123 +++++++++++++++++++++++ apps/server/src/vcs/GitVcsDriver.ts | 17 +++- 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index d8dfaa27b626..f819ad36f7ad 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -13,12 +13,14 @@ import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/process"; +import * as Metric from "effect/Metric"; import { assert, it } from "@effect/vitest"; import { CheckpointRef, GitCommandError, VcsProcessExitError } from "@t3tools/contracts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import * as ServerConfig from "../config.ts"; +import { gitCommandDuration, gitCommandsTotal } from "../observability/Metrics.ts"; import * as CheckpointStore from "../checkpointing/CheckpointStore.ts"; import * as ProcessRunner from "../processRunner.ts"; import * as GitVcsDriver from "./GitVcsDriver.ts"; @@ -1491,3 +1493,124 @@ it.effect.each(["win32", "linux"] as const)( } }).pipe(Effect.scoped, Effect.provide(layerGitContract)), ); + +const gitCommandCount = (operation: string, outcome: string) => + Metric.value( + Metric.withAttributes(gitCommandsTotal, [ + ["operation", operation], + ["outcome", outcome], + ]), + ).pipe(Effect.map((state) => state.count)); + +const gitCommandDurationCount = (operation: string) => + Metric.value(Metric.withAttributes(gitCommandDuration, [["operation", operation]])).pipe( + Effect.map((state) => state.count), + ); + +const makeMeteredDriver = (exitCode: number, failure?: VcsProcessExitError) => + GitVcsDriver.makeVcsDriverShape().pipe( + Effect.provideService(VcsProcess.VcsProcess, { + run: () => + failure === undefined + ? Effect.succeed({ + exitCode: ChildProcessSpawner.ExitCode(exitCode), + stdout: "", + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + }) + : Effect.fail(failure), + }), + ); + +it.effect("GitVcsDriver meters each git command once under its operation", () => + Effect.gen(function* () { + const operation = "GitVcsDriver.checkpoints.test.metered"; + const driver = yield* makeMeteredDriver(0); + + yield* driver.execute({ operation, cwd: "/repo", args: ["status"] }); + yield* driver.execute({ operation, cwd: "/repo", args: ["status"] }); + + assert.strictEqual(yield* gitCommandCount(operation, "success"), 2); + assert.strictEqual(yield* gitCommandCount(operation, "failure"), 0); + assert.strictEqual(yield* gitCommandDurationCount(operation), 2); + }).pipe(Effect.provide(NodeServices.layer)), +); + +it.effect("GitVcsDriver counts an allowed non-zero exit as a success", () => + Effect.gen(function* () { + const operation = "GitVcsDriver.checkpoints.test.allowedNonZero"; + const driver = yield* makeMeteredDriver(1); + + const result = yield* driver.execute({ + operation, + cwd: "/repo", + args: ["diff", "--quiet"], + allowNonZeroExit: true, + }); + + assert.strictEqual(result.exitCode, 1); + assert.strictEqual(yield* gitCommandCount(operation, "success"), 1); + assert.strictEqual(yield* gitCommandCount(operation, "failure"), 0); + }).pipe(Effect.provide(NodeServices.layer)), +); + +it.effect("GitVcsDriver counts a failed git command as a failure", () => + Effect.gen(function* () { + const operation = "GitVcsDriver.checkpoints.test.failed"; + const driver = yield* makeMeteredDriver( + 128, + new VcsProcessExitError({ + operation, + command: "git", + cwd: "/repo", + exitCode: 128, + detail: "fatal", + }), + ); + + const error = yield* driver + .execute({ operation, cwd: "/repo", args: ["rev-parse", "HEAD"] }) + .pipe(Effect.flip); + + assert.strictEqual(error._tag, "VcsProcessExitError"); + assert.strictEqual(yield* gitCommandCount(operation, "failure"), 1); + assert.strictEqual(yield* gitCommandCount(operation, "success"), 0); + assert.strictEqual(yield* gitCommandDurationCount(operation), 1); + }).pipe(Effect.provide(NodeServices.layer)), +); + +it.effect("checkpoint capture meters every git process it starts, including streamed ones", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const liveProcess = yield* VcsProcess.VcsProcess; + const operation = VcsProcess.CHECKPOINT_CAPTURE_OPERATION; + let started = 0; + let streamed = 0; + const driver = yield* GitVcsDriver.makeVcsDriverShape(); + const captureDriver = yield* GitVcsDriver.makeVcsDriverShape().pipe( + Effect.provideService(VcsProcess.VcsProcess, { + run: (input) => { + if (input.operation === operation) { + started++; + if (input.onStdoutChunk !== undefined) streamed++; + } + return liveProcess.run(input); + }, + }), + ); + const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-checkpoint-metrics-" }); + const { checkpointRef } = yield* makeCheckpointFixture(driver, cwd); + const before = + (yield* gitCommandCount(operation, "success")) + + (yield* gitCommandCount(operation, "failure")); + + yield* captureDriver.checkpoints.captureCheckpoint({ cwd, checkpointRef }); + + const after = + (yield* gitCommandCount(operation, "success")) + + (yield* gitCommandCount(operation, "failure")); + assert.isTrue(streamed > 0); + assert.strictEqual(after - before, started); + }).pipe(Effect.scoped, Effect.provide(layerGitContract)), +); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 684285d608d5..7bceab4de831 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -35,6 +35,7 @@ import { } from "@t3tools/contracts"; import { isMissingExecutable } from "@t3tools/source-control-core/server/discovery"; import * as HostProcess from "@t3tools/shared/HostProcess"; +import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../observability/Metrics.ts"; import { makeGitVcsDriverCore, PATCH_RENDER_PREFIX_ARGS, @@ -503,6 +504,18 @@ function parseGitRemoteVerboseOutput( return remotes; } +const runGitProcess = ( + process: VcsProcess.VcsProcess["Service"], + input: VcsProcess.VcsProcessInput, +) => + process.run(input).pipe( + withMetrics({ + counter: gitCommandsTotal, + timer: gitCommandDuration, + attributes: { operation: input.operation }, + }), + ); + const gitCommand = ( process: VcsProcess.VcsProcess["Service"], operation: string, @@ -518,7 +531,7 @@ const gitCommand = ( readonly appendTruncationMarker?: boolean; }, ) => - process.run({ + runGitProcess(process, { operation, command: "git", args: ["-C", cwd, ...args], @@ -903,7 +916,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( let skipped = false; let skippedRecord: number[] = []; const skippedPaths: string[] = []; - yield* vcsProcess.run({ + yield* runGitProcess(vcsProcess, { operation, command: "git", cwd: input.cwd,