agoda-com / agoda-com/kotlin-local-metrics

Start Repo

Ouverte
#1 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
Langage dominant
Kotlin
Étoiles
0
Forks
0
Métriques de merge des PR
Aucune PR mergée en 30 j

Description

You've already got half of this in the wild, by the way — agoda-com/java-local-metrics covers JUnit test metrics via a run listener, with Gradle and sbt setups — so the genuinely missing pieces for Kotlin/Gradle/Ktor are the **build (compilation) metrics** and the **startup metrics**. Here's how I'd structure it so it mirrors the .NET repo's shape and lands in the same datastore.

## 1. Build metrics → a Gradle Settings plugin using the Tooling API listener

The MSBuild task equivalent in Gradle land is **not** a task — it's a `BuildService` implementing `OperationCompletionListener`. This is the configuration-cache-safe, Gradle-8+ blessed way to observe task execution:

```kotlin
abstract class BuildMetricsService :
BuildService,
OperationCompletionListener, AutoCloseable {

interface Params : BuildServiceParameters {
val endpoint: Property
}

private val taskDurations = ConcurrentHashMap()

override fun onFinish(event: FinishEvent) {
if (event is TaskFinishEvent) {
taskDurations[event.descriptor.taskPath] =
event.result.endTime - event.result.startTime
}
}

override fun close() {
// build finished — aggregate per-project compile time and POST
postMetrics(taskDurations)
}
}
```

Register it in a **settings plugin** (`Settings.gradle.kts` scope, applied once per build rather than per project — this is the "install the NuGet package in every project" pain point solved for free):

```kotlin
class BuildMetricsPlugin @Inject constructor(
private val registry: BuildEventsListenerRegistry
) : Plugin {
override fun apply(settings: Settings) {
val service = settings.gradle.sharedServices.registerIfAbsent(
"buildMetrics", BuildMetricsService::class) {
parameters.endpoint.set(
System.getenv("BUILD_METRICS_ES_ENDPOINT")
?: "http://compilation-metrics/gradle")
}
registry.onTaskCompletion(service)
}
}
```

Design decisions worth copying from the .NET version:

- **Filter to what matters.** Flag `KotlinCompile` / `JavaCompile` / `KaptTask` tasks specifically so you can report "compilation time" as distinct from total build time. Report both — total wall-clock and compile-only — plus incremental vs clean (infer from up-to-date task counts).
- **Same payload schema.** Match `dotnetbuild.json` field-for-field where possible (hostname, username, project name, duration, git branch/sha, IDE detection via `idea.active` sysprop) so it lands in the same ES index and your dashboards work across stacks immediately.
- **Fail silently, fast.** Fire the POST with a ~2s timeout on a daemon thread, swallow everything. A metrics tool that ever slows down or breaks a build is dead on arrival with 100+ engineers.
- **Local-only guard.** Skip when `CI=true` / GitLab CI env vars are present — you already have CI telemetry; this is about the F5 experience.
- **Distribution:** publish to your internal Maven, then auto-apply via an init script pushed with your standard dev-machine setup, or bake it into your convention/settings plugin that repos already apply. The init-script route means zero per-repo adoption work, which at your team count is the difference between 90% and 15% coverage.

One alternative to know about: Gradle 8.1+ has the **Flow API** (`FlowAction`/`FlowProviders.buildWorkResult`) for "build finished" hooks, but the BuildService `close()` approach above works across a wider Gradle version range and gets you task-level data anyway.

## 2. Startup metrics → a Ktor `ApplicationPlugin`

The `AspNetStartup` equivalent maps almost one-to-one onto Ktor's application events:

```kotlin
val StartupMetrics = createApplicationPlugin("StartupMetrics") {
val jvmStart = ManagementFactory.getRuntimeMXBean().startTime
var firstResponseSent = false

on(MonitoringEvent(ApplicationStarted)) {
val startupMs = System.currentTimeMillis() - jvmStart
post(type = ".KtorStartup", value = startupMs) // flavor 1
}

onCallRespond { _ ->
if (!firstResponseSent) {
firstResponseSent = true
post(type = ".KtorResponse",
value = System.currentTimeMillis() - jvmStart) // flavor 2
}
}
}
```

Two things to mirror from the .NET package: gate it on a dev environment check (Ktor has no `ASPNETCORE_ENVIRONMENT`, so key off `io.ktor.development=true` or your own env var), and prefer JVM start time as the baseline rather than plugin-install time — it captures classpath scanning and DI wiring, which is where Ktor apps actually spend their startup. The .NET version's "hosting startup assemblies" trick (zero code changes in the app) doesn't have a perfect Ktor analogue; the closest is shipping it inside your internal Ktor convention/bootstrap library so teams get it implicitly.

## 3. Test metrics → JUnit Platform `LauncherSessionListener` via ServiceLoader

The existing java-local-metrics listener requires a VM option added to the JUnit run configuration, which is friction. For JUnit 5 you can do better: implement `TestExecutionListener` and register it via `META-INF/services/org.junit.platform.launcher.TestExecutionListener`. Then it activates automatically the moment the jar is on the test runtime classpath — works identically from Gradle, IntelliJ, and Kotest (which runs on the JUnit Platform). That's the "install package and you're done" experience the .NET repo has.

## Suggested repo shape

A `kotlin-local-metrics` (or fold into java-local-metrics) mono-repo with three modules: `gradle-build-metrics-plugin`, `ktor-startup-metrics`, `junit5-test-metrics`, plus a shared `metrics-core` holding the payload models and the fire-and-forget HTTP client — same Swiss-army-knife structure as the .NET repo, one artifact per concern.

The trickiest part of the whole exercise is honestly the Gradle plugin's config-cache compatibility (no `Project` references inside the service, everything through `Property` params) — worth writing a functional test with TestKit against config-cache-on early, before the API shape ossifies. Happy to draft the full plugin with the POST payload matching your existing ES schema if you want a working starting point.

Guide de contribution

Ouvrir le guide de contribution

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.