Kora Telemetry Metrics
Kora sub-skill — obey the kora-v1 meta rules on every task: R0 ensure
.kora-agent/docs+examples are cloned · R1 read this sub-skill before writing code · R2 Kora APIs only — no Spring/Micronaut/Quarkus, no invented annotations or config keys · R3 journal any incorrect Kora usage. Add comments/Javadoc only if asked.
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.19")
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.19"))
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']
See Metrics Export Reference.
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.