ワンクリックで
deprecated-observability-setup-api
Setting up Prometheus metrics, OpenTelemetry tracing, and health endpoints for Nais applications
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Setting up Prometheus metrics, OpenTelemetry tracing, and health endpoints for Nais applications
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Gi nye apper tilgang til Lumi feedback — ruter Azure via proxy og TokenX direkte til API
Exposed 1.0.0 JDBC DSL patterns (imports, transactions, queries, raw SQL, and types)
Database migration patterns using Flyway with versioned SQL scripts
Sealed class configuration pattern for Kotlin applications with environment-specific settings
Responsive layout patterns using Aksel spacing tokens with Box, VStack, HStack, and HGrid
Observability setup for lumi-dashboard (TanStack Start/Node.js) on Nais
| name | deprecated-observability-setup-api |
| description | Setting up Prometheus metrics, OpenTelemetry tracing, and health endpoints for Nais applications |
DEPRECATED: use
.github/skills/observability-setup-api/skill.mdat repo root.
This skill provides patterns for setting up observability in Nais applications.
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
fun Application.configureHealthEndpoints() {
routing {
route("/internal") {
get("/isAlive") {
call.respondText("OK", ContentType.Text.Plain)
}
get("/isReady") {
call.respondText("OK", ContentType.Text.Plain)
}
}
}
}
fun checkDatabase(dataSource: HikariDataSource): Boolean {
return try {
dataSource.connection.use { it.isValid(1) }
} catch (e: Exception) {
false
}
}
Kafka is not used in `lumi-api`; do not add Kafka readiness checks unless the repo is explicitly extended with Kafka.
import io.micrometer.core.instrument.Clock
import io.micrometer.core.instrument.binder.jvm.*
import io.micrometer.prometheus.PrometheusConfig
import io.micrometer.prometheus.PrometheusMeterRegistry
import io.prometheus.client.CollectorRegistry
import io.ktor.server.metrics.micrometer.*
import io.ktor.server.response.*
import io.ktor.http.*
val meterRegistry = PrometheusMeterRegistry(
PrometheusConfig.DEFAULT,
CollectorRegistry.defaultRegistry,
Clock.SYSTEM
)
In this repo, metrics are exposed at `GET /internal/prometheus` (see `InternalRoutes.kt`).
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.Timer
class UserService(private val meterRegistry: PrometheusMeterRegistry) {
private val userCreatedCounter = Counter.builder("users_created_total")
.description("Total users created")
.register(meterRegistry)
private val userCreationTimer = Timer.builder("user_creation_duration_seconds")
.description("User creation duration")
.register(meterRegistry)
fun createUser(user: User) {
userCreationTimer.record {
repository.save(user)
}
userCreatedCounter.increment()
}
}
Nais enables OpenTelemetry auto-instrumentation by default. For manual spans:
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.StatusCode
val tracer = GlobalOpenTelemetry.getTracer("my-app")
fun processPayment(paymentId: String) {
val span = tracer.spanBuilder("processPayment")
.setAttribute("payment.id", paymentId)
.startSpan()
try {
// Business logic
val payment = repository.findPayment(paymentId)
span.setAttribute("payment.amount", payment.amount)
processPaymentInternal(payment)
span.setStatus(StatusCode.OK)
} catch (e: Exception) {
span.setStatus(StatusCode.ERROR, "Payment processing failed")
span.recordException(e)
throw e
} finally {
span.end()
}
}
import mu.KotlinLogging
import net.logstash.logback.argument.StructuredArguments.kv
private val logger = KotlinLogging.logger {}
fun processOrder(orderId: String) {
logger.info(
"Processing order",
kv("order_id", orderId),
kv("timestamp", LocalDateTime.now())
)
try {
orderService.process(orderId)
logger.info(
"Order processed successfully",
kv("order_id", orderId)
)
} catch (e: Exception) {
logger.error(
"Order processing failed",
kv("order_id", orderId),
kv("error", e.message),
e
)
throw e
}
}
apiVersion: nais.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: myteam
labels:
team: myteam
spec:
image: ghcr.io/navikt/my-app:latest
port: 8080
# Health checks
liveness:
path: /internal/isAlive
initialDelay: 10
timeout: 1
periodSeconds: 10
failureThreshold: 3
readiness:
path: /internal/isReady
initialDelay: 10
timeout: 1
periodSeconds: 10
failureThreshold: 3
# Prometheus scraping
prometheus:
enabled: true
path: /internal/prometheus
# OpenTelemetry auto-instrumentation
observability:
autoInstrumentation:
enabled: true
runtime: java # Instruments Ktor, JDBC, Kafka automatically
logging:
destinations:
- id: loki # Automatic Loki shipping
- id: team-logs # Optional: private team logs
# Resources (for metrics alerting)
resources:
limits:
memory: 512Mi
requests:
cpu: 50m
memory: 256Mi
Create nais/app/alert.yml:
apiVersion: nais.io/v1
kind: Alert
metadata:
name: my-app-alerts
namespace: myteam
labels:
team: myteam
spec:
receivers:
slack:
channel: "#team-alerts"
prependText: "@here "
alerts:
- alert: HighErrorRate
expr: |
(sum(rate(http_requests_total{app="my-app",status=~"5.."}[5m]))
/ sum(rate(http_requests_total{app="my-app"}[5m]))) > 0.05
for: 5m
description: "Error rate is {{ $value | humanizePercentage }}"
action: "Check logs in Grafana Loki"
documentation: https://teamdocs/runbooks/high-error-rate
sla: "Respond within 15 minutes"
severity: critical
- alert: HighResponseTime
expr: |
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket{app="my-app"}[5m])
) > 1
for: 10m
description: "95th percentile response time is {{ $value }}s"
action: "Check Tempo traces for slow requests"
severity: warning
- alert: PodCrashLooping
expr: |
rate(kube_pod_container_status_restarts_total{
pod=~"my-app-.*"
}[15m]) > 0
for: 5m
description: "Pod {{ $labels.pod }} is crash looping"
action: "Check logs: kubectl logs {{ $labels.pod }}"
severity: critical
- alert: HighMemoryUsage
expr: |
(container_memory_working_set_bytes{app="my-app"}
/ container_spec_memory_limit_bytes{app="my-app"}) > 0.9
for: 10m
description: "Memory usage is {{ $value | humanizePercentage }}"
action: "Check for memory leaks, increase limits if needed"
severity: warning
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.micrometer.core.instrument.Timer
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.StatusCode
fun main() {
val env = Environment.from(System.getenv())
val dataSource = createDataSource(env.databaseUrl)
// Run database migrations
runMigrations(dataSource)
// Setup metrics
val meterRegistry = setupMetrics()
embeddedServer(Netty, port = 8080) {
configureHealthEndpoints(dataSource)
configureMetrics(meterRegistry)
configureRouting(dataSource, meterRegistry)
}.start(wait = true)
}
fun Application.configureRouting(
dataSource: HikariDataSource,
meterRegistry: PrometheusMeterRegistry
) {
val tracer = GlobalOpenTelemetry.getTracer("my-app")
routing {
get("/api/users") {
val requestTimer = Timer.sample()
val requestCounter = meterRegistry.counter(
"http_requests_total",
"method", "GET",
"endpoint", "/api/users"
)
val span = tracer.spanBuilder("getUsersRequest")
.setAttribute("http.method", "GET")
.setAttribute("http.route", "/api/users")
.startSpan()
try {
val users = userRepository.findAll()
span.setAttribute("user.count", users.size.toLong())
span.setStatus(StatusCode.OK)
requestCounter.increment()
requestTimer.stop(meterRegistry.timer(
"http_request_duration_seconds",
"method", "GET",
"endpoint", "/api/users",
"status", "200"
))
call.respond(users)
} catch (e: Exception) {
span.setStatus(StatusCode.ERROR, "Failed to get users")
span.recordException(e)
meterRegistry.counter(
"http_requests_total",
"method", "GET",
"endpoint", "/api/users",
"status", "500"
).increment()
logger.error(
"Failed to get users",
kv("trace_id", span.spanContext.traceId),
kv("span_id", span.spanContext.spanId),
e
)
throw e
} finally {
span.end()
}
}
}
}
Create a dashboard in Grafana with these panels:
Panel 1: Request Rate
sum(rate(http_requests_total{app="my-app"}[5m])) by (endpoint)
Panel 2: Error Rate
sum(rate(http_requests_total{app="my-app",status=~"5.."}[5m]))
/ sum(rate(http_requests_total{app="my-app"}[5m])) * 100
Panel 3: Response Time (p50, p95, p99)
histogram_quantile(0.50, rate(http_request_duration_seconds_bucket{app="my-app"}[5m]))
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{app="my-app"}[5m]))
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{app="my-app"}[5m]))
Panel 4: Memory Usage
container_memory_working_set_bytes{app="my-app"}
/ container_spec_memory_limit_bytes{app="my-app"} * 100
Panel 5: Database Connections
hikaricp_connections_active{app="my-app"}
hikaricp_connections_max{app="my-app"}
Panel 6: Kafka Consumer Lag
kafka_consumer_lag{app="my-app"}
View logs in Grafana Loki Explorer:
# All logs from your app
{app="my-app", namespace="myteam"}
# Only errors
{app="my-app"} |= "ERROR"
# JSON logs with specific field
{app="my-app"} | json | event_type="payment_processed"
# Logs correlated with trace
{app="my-app"} | json | trace_id="abc123def456"
# Count errors per minute
sum(rate({app="my-app"} |= "ERROR" [1m])) by (pod)
View traces in Grafana Tempo:
my-appgetUsersRequest> 1serrorOr link from logs by clicking trace_id in Loki.
/internal/isAlive endpoint implemented/internal/isReady endpoint implemented/internal/prometheus endpoint exposing Prometheus metricsnais/app/alert.ymlBased on 177+ repositories using observability setup:
import io.micrometer.core.instrument.binder.jvm.*
install(MicrometerMetrics) {
registry = meterRegistry
meterBinders = listOf(
JvmMemoryMetrics(), // Heap, non-heap, buffer pool metrics
JvmGcMetrics(), // GC pause time, count
ProcessorMetrics(), // CPU usage
UptimeMetrics() // Application uptime
)
}
// From dp-rapportering: Track business events
val eventsProcessed = Counter.builder("events_processed_total")
.description("Total events processed")
.tag("event_type", "rapportering_innsendt")
.tag("status", "ok")
.register(meterRegistry)
// From dp-rapportering: Track API errors
val apiErrors = Counter.builder("api_errors_total")
.description("Total API errors")
.tag("endpoint", "/api/rapporteringsperioder")
.tag("error_type", "validation_error")
.register(meterRegistry)
// From dp-rapportering: Measure HTTP call duration
suspend fun <T> timedAction(navn: String, block: suspend () -> T): T {
val (result, duration) = measureTimedValue {
block()
}
Timer.builder("http_timer")
.tag("navn", navn)
.description("HTTP call duration")
.register(meterRegistry)
.record(duration.inWholeMilliseconds, MILLISECONDS)
return result
}
Track DORA metrics for your team:
// Deployment frequency
val deployments = Counter.builder("deployments_total")
.description("Total deployments")
.tag("team", "myteam")
.tag("environment", "production")
.register(meterRegistry)
// Lead time for changes (commit to deploy)
val leadTime = Timer.builder("deployment_lead_time_seconds")
.description("Time from commit to deployment")
.tag("team", "myteam")
.register(meterRegistry)
// Change failure rate
val failedDeployments = Counter.builder("deployments_failed_total")
.description("Total failed deployments")
.tag("team", "myteam")
.register(meterRegistry)
// Time to restore service
val incidentResolutionTime = Timer.builder("incident_resolution_duration_seconds")
.description("Time to resolve incidents")
.tag("team", "myteam")
.tag("severity", "critical")
.register(meterRegistry)
Alert on DORA metrics:
- alert: LowDeploymentFrequency
expr: |
sum(increase(deployments_total{team="myteam",environment="production"}[7d]))
< 5
description: "Only {{ $value }} deployments in last 7 days (target: >1/day)"
severity: info
- alert: HighChangeFailureRate
expr: |
sum(rate(deployments_failed_total{team="myteam"}[7d]))
/ sum(rate(deployments_total{team="myteam"}[7d]))
> 0.15
description: "Change failure rate is {{ $value | humanizePercentage }} (target: <15%)"
severity: warning
See https://dora.dev for benchmarks and best practices.