Kora telemetry metrics
Skill kora-projects/kora-skills/plugins/kora-v1/skills/kora-telemetry-metrics
Agent Skills for Kora Framework — compile-time DI for Java/Kotlin backend development.
npx -y skills add kora-projects/kora-skills --skill kora-telemetry-metricsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Kora Micrometer metrics via the micrometer-module — MetricsModule wires a PrometheusMeterRegistry and a MeterRegistry into the graph for custom Counter/Gauge/Timer/DistributionSummary, exposed in Prometheus format on the private HTTP port (privateApiHttpMetricsPath). Covers MetricsConfig (metrics.opentelemetrySpec V120/V123), per-module telemetry.metrics.slo buckets, PrometheusMeterRegistryInitializer for common tags, and tag-cardinality safety. Use when adding business metrics, configuring the /metrics scrape endpoint, tuning latency buckets, or debugging missing/high-cardinality metrics.
SKILL.md
12.8 KB, ~2.8k tokens by cl100k_base, as published. Nobody here has run it
Kora Telemetry Metrics
Kora collects metrics with Micrometer. Adding MetricsModule registers a PrometheusMeterRegistry, instruments every Kora module (HTTP, gRPC, JDBC, Kafka, cache, JVM), and publishes a MeterRegistry component you inject to record custom business metrics. Metrics are served in Prometheus text format on the private HTTP port — never the public one.
Quick Start
1. Add the dependency
The micrometer-module already brings the PrometheusMeterRegistry; no separate registry artifact is needed. All ru.tinkoff.kora:* versions come from the kora-parent BOM — never pin them individually.
===! ":fontawesome-brands-java: Java"
```groovy
dependencies {
koraBom platform("ru.tinkoff.kora:kora-parent:1.2.17")
annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:micrometer-module"
implementation "ru.tinkoff.kora:http-server-undertow"
implementation "ru.tinkoff.kora:config-hocon"
implementation "ru.tinkoff.kora:logging-logback"
}
```
=== ":simple-kotlin: Kotlin"
```kotlin
dependencies {
koraBom(platform("ru.tinkoff.kora:kora-parent:1.2.17"))
ksp("ru.tinkoff.kora:symbol-processors")
implementation("ru.tinkoff.kora:micrometer-module")
implementation("ru.tinkoff.kora:http-server-undertow")
implementation("ru.tinkoff.kora:config-hocon")
implementation("ru.tinkoff.kora:logging-logback")
}
```
2. Add MetricsModule to the graph
===! ":fontawesome-brands-java: Java"
```java
import ru.tinkoff.kora.application.graph.KoraApplication;
import ru.tinkoff.kora.common.KoraApp;
import ru.tinkoff.kora.config.hocon.HoconConfigModule;
import ru.tinkoff.kora.http.server.undertow.UndertowHttpServerModule;
import ru.tinkoff.kora.logging.logback.LogbackModule;
import ru.tinkoff.kora.micrometer.module.MetricsModule;
@KoraApp
public interface Application extends
HoconConfigModule,
LogbackModule,
MetricsModule,
UndertowHttpServerModule {
static void main(String[] args) {
KoraApplication.run(ApplicationGraph::graph);
}
}
```
=== ":simple-kotlin: Kotlin"
```kotlin
import ru.tinkoff.kora.common.KoraApp
import ru.tinkoff.kora.config.hocon.HoconConfigModule
import ru.tinkoff.kora.http.server.undertow.UndertowHttpServerModule
import ru.tinkoff.kora.logging.logback.LogbackModule
import ru.tinkoff.kora.micrometer.module.MetricsModule
@KoraApp
interface Application :
HoconConfigModule,
LogbackModule,
MetricsModule,
UndertowHttpServerModule
```
3. Expose the scrape endpoint on the private port
httpServer {
publicApiHttpPort = 8080 # business traffic
privateApiHttpPort = 8085 # metrics, probes, admin
privateApiHttpMetricsPath = "/metrics"
}
metrics {
opentelemetrySpec = "V120" # or "V123"
}
Scrape and verify:
curl http://localhost:8085/metrics
4. Record a custom metric
Inject MeterRegistry through the constructor (it is a normal graph component) and register meters once.
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import ru.tinkoff.kora.common.Component;
import java.time.Duration;
@Component
public final class MetricsService {
private final Timer userCreationTimer;
private final Counter userCreationCounter;
public MetricsService(MeterRegistry meterRegistry) {
this.userCreationTimer = Timer.builder("user.creation.duration")
.description("Time taken to create users")
.serviceLevelObjectives(
Duration.ofMillis(50),
Duration.ofMillis(100),
Duration.ofMillis(250),
Duration.ofMillis(500))
.register(meterRegistry);
this.userCreationCounter = Counter.builder("user.creation.total")
.description("Total number of users created")
.register(meterRegistry);
}
public void createUser(Runnable work) {
userCreationTimer.record(work);
userCreationCounter.increment();
}
}
What's in references/ and assets/
| File | Purpose |
|---|---|
| references/micrometer-types-reference.md | Choosing Counter / Gauge / Timer / DistributionSummary, builder APIs |
| references/custom-metrics-reference.md | Business-metric patterns, dynamic-tag caching, full payment example |
| references/metrics-config-reference.md | metrics.opentelemetrySpec, PrometheusMeterRegistryInitializer, per-module telemetry.metrics |
| references/metrics-cardinality-reference.md | Good vs bad tags, memory-leak prevention, cardinality checklist |
| references/metrics-export-reference.md | Prometheus pull model, scrape config, verification |
| references/metrics-reference.md | Catalogue of built-in Kora metrics with real OpenTelemetry tag names |
| assets/ | Application, config, service, and monitoring templates |
Metric Types
| Type | Use Case | Example |
|---|---|---|
| Counter | Monotonically increasing event count | Counter.builder("requests.total").register(registry).increment() |
| Gauge | Current point-in-time value | Gauge.builder("queue.size", queue, Queue::size).register(registry) |
| Timer | Duration / latency | Timer.builder("request.duration").register(registry).record(() -> {...}) |
| DistributionSummary | Distribution of arbitrary values (sizes) | summary.record(payloadBytes) |
See Micrometer Types Reference.
Common tags for all metrics
To stamp every metric with shared labels, add a PrometheusMeterRegistryInitializer as a default method in a @Module. It is a Function<PrometheusMeterRegistry, PrometheusMeterRegistry>, so it must return the registry. It is applied exactly once at application start.
===! ":fontawesome-brands-java: Java"
```java
import ru.tinkoff.kora.common.Module;
import ru.tinkoff.kora.micrometer.module.PrometheusMeterRegistryInitializer;
@Module
public interface MetricsConfigModule {
default PrometheusMeterRegistryInitializer commonTagsInit() {
return registry -> {
registry.config().commonTags("service", "order-service", "environment", "production");
return registry;
};
}
}
```
=== ":simple-kotlin: Kotlin"
```kotlin
import ru.tinkoff.kora.common.Module
import ru.tinkoff.kora.micrometer.module.PrometheusMeterRegistryInitializer
@Module
interface MetricsConfigModule {
fun commonTagsInit(): PrometheusMeterRegistryInitializer {
return PrometheusMeterRegistryInitializer { registry ->
registry.config().commonTags("service", "order-service", "environment", "production")
registry
}
}
}
```
Histogram buckets (SLO targets)
Bucketed measurements let monitoring compute "how many calls were under 100 ms?" and percentile estimates. Configure them with serviceLevelObjectives(...) on the builder; the values are your business latency targets, not universal defaults.
Timer.builder("api.request.duration")
.serviceLevelObjectives(
Duration.ofMillis(50),
Duration.ofMillis(100),
Duration.ofMillis(250),
Duration.ofMillis(500),
Duration.ofSeconds(1))
.register(registry);
Built-in Kora module metrics expose the same control through config as a millisecond array: <module>.telemetry.metrics.slo = [ 1, 10, 50, 100, 200, 500, 1000, ... ]. See Metrics Config Reference.
Tag cardinality (critical)
Each unique tag-value combination creates a separate time series. Tag values must be a bounded, finite set, otherwise the registry grows without limit and the process leaks memory.
| Safe tags (bounded) | Dangerous tags (unbounded) |
|---|---|
method (GET, POST) | userId |
status (200, 404, 500) | requestId (UUID per call) |
http.route template (/users/{id}) | raw path (/users/128734) |
provider (gmail.com, yahoo.com) | email (one per user) |
For a runtime tag with a bounded value set, cache the meter per value instead of rebuilding it on every call:
private final ConcurrentHashMap<String, Counter> counters = new ConcurrentHashMap<>();
private Counter counter(String provider) {
return counters.computeIfAbsent(provider, p ->
Counter.builder("user.creation.total")
.tag("email.provider", p)
.register(registry));
}
See Cardinality Reference and Custom Metrics Reference.
Export model — Prometheus pull only
Kora's micrometer-module registers a PrometheusMeterRegistry and serves it on privateApiHttpMetricsPath. The export model is pull: a Prometheus server scrapes the private port. There is no Kora configuration for switching the registry to a push exporter (StatsD/OTLP/etc.) — to ship to another backend, scrape the endpoint with an agent (Prometheus, OpenTelemetry Collector, vmagent) and forward from there. For distributed tracing export, use the separate tracing modules (kora-telemetry-tracing), which are unrelated to the metric registry.
# prometheus.yml
scrape_configs:
- job_name: 'order-service'
scrape_interval: 15s
metrics_path: '/metrics'
static_configs:
- targets: ['order-service:8085']
Built-in Kora metrics
After MetricsModule is connected, Kora instruments its modules automatically using OpenTelemetry semantic-convention names and tags.
| Module | Key metric | Type |
|---|---|---|
| HTTP Server | http.server.request.duration, http.server.active_requests | DistributionSummary, Gauge |
| HTTP Client | http.client.request.duration | DistributionSummary |
| Database | db.client.request.duration | DistributionSummary |
| Kafka | messaging.receive.duration, messaging.publish.duration, messaging.kafka.consumer.lag | DistributionSummary, Gauge |
| gRPC | rpc.server.duration, rpc.client.duration | DistributionSummary |
| Cache | cache.duration, cache.ratio | DistributionSummary, Counter |
| System | kora.up, jvm.memory.used, jvm.gc.pause, process.cpu.usage | Gauge / DistributionSummary |
The full catalogue with exact tag names is in Metrics Reference.
Common pitfalls
| Symptom | Fix |
|---|---|
/metrics returns 404 | Add the HTTP server module and set privateApiHttpMetricsPath; scrape the private port |
| Metrics not appearing | Confirm MetricsModule is in @KoraApp extends ... and MeterRegistry is injected |
| Memory grows continuously | Unbounded tag (userId, requestId, raw URL) — drop it or map to a bounded value |
| Initializer doesn't compile | PrometheusMeterRegistryInitializer must return the registry |
| Wrong metric type | Counter/Gauge/Timer/DistributionSummary — choose based on metric semantics |
| Metrics on the public port | Always keep /metrics on privateApiHttpPort |
Looking for probes module | No such module — probes are in HTTP server module (see Probes Reference) |
Verification
After build (annotation processors generate ApplicationGraph), run the service and:
curl -s http://localhost:8085/metrics | grep kora_up
kora_up 1.0 confirms the framework is running and the registry is exposed.
Gives 0 of the 12 instructions most monitoring observability skills give in ~2.8k tokens
Counted across 481 of the 483 authors here whose files we hold, read 2026-08-06
- link every alert to a runbookin 43 of 481, across 35 files
- use structured json loggingin 36 of 481, across 31 files
- alert on user-facing symptomsin 20 of 481, across 15 files
- emit structured JSON logs with stable event namesin 18 of 481, across 13 files
- propagate trace context across boundariesin 16 of 481
- use histograms for latency trackingin 14 of 481, across 9 files
- use OpenTelemetry for distributed tracingin 13 of 481, across 8 files
- include a correlation ID on every log linein 13 of 481, across 8 files
- Define service level objectivesin 10 of 481, across 7 files
- Call useAzureMonitor before importing other modulesin 9 of 481, across 2 files
- stop and ask for clarification if inputs are missingin 9 of 481, across 2 files
- define on-call questions before adding telemetryin 9 of 481, across 4 files
Said here and by no other author read
- add MetricsModule to the application graph
- serve metrics on the private http port
- set privateApiHttpMetricsPath to the scrape path
- inject MeterRegistry to record custom metrics
- register custom meters exactly once
- configure histogram buckets using service level objectives
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.