Pada aplikasi Nuxt 3, masalah performa dan error produksi sering tidak terlihat jelas hanya dari log teks atau metrik dasar. Request bisa dimulai dari browser, masuk ke server-side rendering (SSR), memanggil API internal atau eksternal, lalu gagal di salah satu titik tanpa jejak yang mudah diikuti. Di sinilah observability menjadi penting: bukan hanya mengumpulkan data, tetapi membangun kemampuan untuk menjawab pertanyaan operasional seperti "kenapa halaman ini lambat?", "request mana yang gagal?", atau "error di frontend ini berkaitan dengan trace backend yang mana?".
Dalam artikel ini kita akan membangun pendekatan observability end-to-end untuk Nuxt 3 dengan kombinasi OpenTelemetry untuk instrumentasi tracing, Loki untuk agregasi log, dan Tempo untuk penyimpanan trace. Stack ini cocok bila Anda ingin korelasi antara log dan trace tanpa harus bergantung pada platform observability komersial. Fokusnya adalah implementasi praktis, bukan sekadar konsep.
Kenapa observability Nuxt 3 perlu dipikirkan dari awal
Nuxt 3 menjalankan aplikasi di dua sisi: browser dan server. Dalam mode SSR, satu request halaman dapat memicu beberapa tahap:
- Request masuk ke server Nuxt/Nitro.
- Rendering komponen dan layout di server.
- Pemanggilan API internal atau upstream service.
- Pengiriman HTML ke browser.
- Hydration di client dan request lanjutan dari browser.
Jika salah satu tahap lambat, pengguna hanya melihat halaman terasa berat. Tanpa trace, tim biasanya menebak-nebak: apakah lambat di database, API upstream, rendering Vue, atau serialisasi payload? Dengan tracing, setiap tahap dapat direpresentasikan sebagai span dalam satu trace.
Sementara itu, log tetap penting karena membawa konteks error, payload yang sudah disanitasi, dan kejadian operasional yang tidak selalu tepat diwakili metrik. Loki cocok untuk log terstruktur dan integrasi yang baik dengan Grafana. Tempo berguna untuk menyimpan trace dengan biaya relatif efisien karena fokus pada trace, bukan indexing berat seperti beberapa sistem lain.
Arsitektur end-to-end: browser, Nuxt server, Loki, dan Tempo
Arsitektur sederhana yang umum dipakai:
- Client browser mengirim trace untuk navigasi, fetch, dan event tertentu.
- Server Nuxt 3/Nitro membuat span untuk request SSR, middleware, route API, dan panggilan keluar.
- OpenTelemetry Collector menerima OTLP trace/log dan meneruskan ke backend observability.
- Tempo menyimpan trace.
- Loki menyimpan log terstruktur.
- Grafana dipakai untuk eksplorasi log, trace, dan dashboard dasar.
Dalam setup produksi, OpenTelemetry Collector sebaiknya dijadikan titik masuk tunggal, bukan mengirim data langsung dari aplikasi ke Tempo atau Loki. Alasannya:
- Memudahkan perubahan backend tanpa ubah kode aplikasi.
- Mendukung batching, retry, sampling, dan enrichment.
- Mengurangi coupling antara aplikasi dan vendor/storage observability.
Korelasi frontend-server
Target penting observability Nuxt 3 adalah menghubungkan trace dari browser ke SSR request di server. Cara yang umum adalah meneruskan header trace context seperti traceparent pada request dari client ke server dan antar-service. OpenTelemetry mengikuti standar W3C Trace Context, sehingga berbagai komponen bisa ikut dalam trace yang sama selama propagasi context berjalan benar.
Untuk SSR, request awal dari browser ke server bisa menjadi root span atau child span, tergantung dari mana trace dimulai. Untuk navigasi client-side setelah hydration, request fetch ke API dapat meneruskan context agar trace tetap tersambung.
Setup dasar OpenTelemetry pada server Nuxt 3
Nuxt 3 berjalan di atas Nitro. Instrumentasi paling aman biasanya dilakukan di sisi runtime server, bukan di komponen Vue secara langsung. Tujuannya adalah menangkap lifecycle request, panggilan HTTP, dan route handler.
Secara umum, Anda membutuhkan:
- SDK OpenTelemetry untuk Node.js.
- Exporter OTLP ke Collector.
- Instrumentation HTTP/fetch sesuai kebutuhan.
- Hook Nitro atau middleware server untuk membuat span tambahan yang relevan dengan domain aplikasi.
Contoh inisialisasi OpenTelemetry pada proses server Node yang menjalankan Nuxt:
import { NodeSDK } from '@opentelemetry/sdk-node'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
const sdk = new NodeSDK({
resource: resourceFromAttributes({
[SemanticResourceAttributes.SERVICE_NAME]: 'nuxt-web',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development'
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
}),
instrumentations: [
new HttpInstrumentation(),
getNodeAutoInstrumentations()
]
})
sdk.start()Prinsip pentingnya bukan pada nama paket semata, melainkan:
- service.name harus konsisten. Ini adalah identitas utama service di Tempo/Grafana.
- Gunakan resource attributes seperti environment, region, atau version agar trace mudah difilter.
- Pastikan inisialisasi terjadi sebelum server benar-benar mulai menerima request.
Membuat span untuk SSR dan API internal
Auto-instrumentation membantu, tetapi pada Nuxt 3 Anda biasanya tetap perlu span manual untuk tahap yang bernilai diagnostik, misalnya render halaman SSR, pemanggilan composable tertentu, query ke API internal, atau proses serialisasi data.
import { trace, SpanStatusCode } from '@opentelemetry/api'
const tracer = trace.getTracer('nuxt-app')
export default defineEventHandler(async (event) => {
return await tracer.startActiveSpan('api.products.list', async (span) => {
try {
span.setAttribute('app.route', '/api/products')
span.setAttribute('app.user.authenticated', Boolean(event.context.user))
const data = await $fetch('https://api.internal.local/products', {
headers: {
'x-request-id': event.node.req.headers['x-request-id'] || ''
}
})
span.setAttribute('app.products.count', Array.isArray(data) ? data.length : 0)
span.end()
return data
} catch (error) {
span.recordException(error)
span.setStatus({ code: SpanStatusCode.ERROR, message: 'failed to fetch products' })
span.end()
throw error
}
})
})Penamaan span sebaiknya mengikuti pola yang konsisten dan mudah dibaca. Hindari nama terlalu generik seperti request atau handler. Contoh yang baik:
ssr.render.homepageapi.products.listauth.validate_sessionexternal.payment.create_invoice
Span name idealnya menjawab operasi apa yang sedang berlangsung, bukan sekadar file mana yang dieksekusi.
Instrumentasi frontend dan propagasi trace context
Bagian frontend sering diabaikan, padahal banyak masalah performa justru terlihat dari transisi halaman, hydration, dan fetch di browser. Untuk Nuxt 3, Anda dapat memasang plugin client yang membuat span untuk navigasi route dan request HTTP tertentu. Jangan mencoba men-trace semua event DOM secara agresif; volume datanya bisa terlalu besar dan sulit berguna.
Fokuskan pada:
- Navigasi route.
- Fetch ke API penting.
- Error JavaScript yang memengaruhi user journey.
- Web vitals atau metrik frontend sebagai data pendamping, jika diperlukan.
Saat melakukan fetch dari browser ke server, pastikan header trace context ikut terkirim. Banyak library HTTP modern sudah bisa diinstrumentasi, tetapi Anda tetap perlu memverifikasi di network inspector bahwa header seperti traceparent benar-benar muncul.
Kesalahan umum adalah tracing di client aktif, tracing di server aktif, tetapi keduanya membentuk trace terpisah karena propagasi context tidak berjalan.
Logging terstruktur ke Loki
Log yang baik untuk observability bukan sekadar string bebas, melainkan log terstruktur dalam format JSON atau key-value yang konsisten. Agar log bisa dikorelasikan dengan trace, masukkan minimal:
trace_idspan_idservice_namelevelmessagerouteatauoperation
Contoh log JSON di server:
{
"timestamp": "2026-03-30T10:15:00.123Z",
"level": "error",
"service_name": "nuxt-web",
"message": "failed to load product details",
"trace_id": "4f2a8c...",
"span_id": "9bc1d2...",
"route": "/products/[id]",
"product_id": "p-123",
"error_type": "UpstreamTimeout"
}Pengiriman ke Loki biasanya dilakukan melalui agen log seperti Promtail, Grafana Alloy, Fluent Bit, atau melalui OpenTelemetry Collector bila pipeline log Anda sudah distandardisasi di sana. Untuk kebanyakan tim, pendekatan yang praktis adalah:
- Aplikasi menulis log JSON ke stdout.
- Agent/collector membaca stdout/container logs.
- Agent menambahkan label yang stabil seperti
service,env,namespace. - Log dikirim ke Loki.
Jangan menaruh terlalu banyak field dinamis sebagai label Loki. Ini kesalahan yang sangat umum. Label dengan kardinalitas tinggi, seperti user ID, session ID, atau request ID, akan membuat storage dan query menjadi mahal. Simpan field tersebut sebagai isi log, bukan label indeks utama.
Sanitasi data sensitif
Observability yang baik tidak boleh mengorbankan keamanan. Span dan log sering berisi URL, header, query parameter, payload, atau exception yang bisa membawa data sensitif. Terapkan aturan sanitasi sejak awal:
- Jangan log token, cookie, authorization header, atau session secret.
- Redact email, nomor telepon, alamat, atau data PII bila tidak benar-benar dibutuhkan.
- Hindari merekam body request penuh untuk endpoint autentikasi atau pembayaran.
- Simpan identifier internal yang aman, bukan payload mentah.
Contoh aturan praktis: rekam user_id_hash alih-alih email, dan simpan status permintaan seperti payment_method=card tanpa nomor kartu atau data billing mentah.
Mengidentifikasi bottleneck SSR dan latency API
Setelah tracing aktif, nilai utamanya muncul saat Anda membaca struktur trace. Untuk request SSR Nuxt 3, pecah span menjadi beberapa tahap logis:
http.serveratau span request masuk.ssr.middleware.authssr.load.page_dataexternal.catalog.get_productsssr.render.vuessr.serialize.payload
Dari sini Anda bisa menjawab:
- Apakah lambat karena API upstream?
- Apakah rendering komponen server memakan waktu besar?
- Apakah middleware autentikasi sering memblokir?
- Apakah payload SSR terlalu besar sehingga serialisasi mahal?
Untuk latency API, gunakan atribut span yang konsisten, misalnya:
http.methodhttp.routeserver.addressatau upstream hosthttp.response.status_codeerror.typebila gagal
Ini membuat query di Tempo atau Grafana lebih mudah, misalnya mencari semua trace dengan status 500 pada route tertentu, atau semua trace ke upstream tertentu dengan latency di atas ambang batas.
Dashboard dasar yang benar-benar berguna
Dashboard observability tidak perlu rumit di awal. Yang penting adalah mendukung troubleshooting harian. Minimal buat panel berikut di Grafana:
- Request rate per route atau service.
- Error rate HTTP 5xx dan 4xx.
- Latency percentile p50, p95, p99 untuk SSR dan API penting.
- Log error terbaru dari service Nuxt.
- Trace sample lambat atau daftar trace dengan durasi tertinggi.
Untuk Loki, query dasar seperti error per route sangat membantu. Untuk Tempo, hubungkan panel ke trace exemplar atau link langsung ke trace result bila toolchain Anda mendukung integrasi tersebut.
Dashboard sebaiknya dibedakan antara:
- Service overview untuk gambaran umum kesehatan aplikasi.
- Route/API deep dive untuk endpoint atau halaman kritikal.
- Incident dashboard untuk error rate, deployment marker, dan log/trace terbaru.
Workflow incident debugging di produksi
Berikut workflow praktis saat insiden terjadi:
- Lihat lonjakan error rate atau latency pada dashboard.
- Filter service dan route yang paling terdampak.
- Buka log error di Loki untuk melihat exception, route, dan konteks operasional.
- Ambil
trace_iddari log terkait lalu buka trace di Tempo. - Lihat span mana yang paling lambat atau gagal: SSR render, upstream API, cache, atau middleware.
- Bandingkan dengan deployment terbaru, perubahan config, atau ketergantungan upstream.
- Jika perlu, tambahkan span/atribut baru yang hilang untuk insiden berikutnya.
Contoh kasus nyata: pengguna mengeluh halaman produk lambat. Dashboard menunjukkan p95 SSR halaman produk naik tajam. Di Tempo terlihat span external.catalog.get_products memakan 1.8 detik. Di Loki ada log timeout dari upstream catalog, lengkap dengan host tujuan dan status retry. Dari sini tim tahu bottleneck bukan di Vue SSR, melainkan service eksternal yang melemah.
Trade-off, keterbatasan, dan kesalahan umum
Sampling
Tracing penuh untuk semua request bisa mahal. Solusinya adalah sampling, tetapi ada trade-off: Anda mungkin kehilangan trace untuk kasus langka. Untuk produksi, banyak tim memulai dengan head-based sampling moderat, lalu menaikkan sampling untuk route kritikal atau error.
Volume log
Logging terlalu verbose di level info/debug dapat membanjiri Loki. Simpan log yang berorientasi diagnosa, bukan semua event internal. Gunakan level log dengan disiplin.
Span terlalu banyak atau terlalu sedikit
Terlalu sedikit span membuat trace tidak berguna. Terlalu banyak span membuat trace bising dan mahal. Fokus pada batas operasi penting: request masuk, middleware utama, query/API besar, render SSR, cache, dan error path.
Tidak konsisten dalam penamaan
Jika satu tim menamai span getProduct, tim lain product.fetch, dan yang lain lagi api-product, analisis jadi sulit. Buat konvensi penamaan sejak awal dan dokumentasikan.
Penutup
Observability untuk Nuxt 3 paling efektif bila dibangun sebagai alur end-to-end, bukan hanya menambahkan logger atau sekadar menyalakan tracing otomatis. Dengan OpenTelemetry, Anda dapat memetakan perjalanan request dari browser ke server dan service lain. Dengan Loki, Anda menyimpan log terstruktur yang bisa dikorelasikan dengan trace. Dengan Tempo, Anda mendapatkan visibilitas terhadap bottleneck SSR, latency API, dan akar error produksi.
Mulailah dari hal kecil namun bernilai: tetapkan service.name yang konsisten, kirim trace ke Collector, buat span manual untuk operasi SSR penting, tulis log JSON dengan trace_id, sanitasi data sensitif, lalu bangun dashboard dasar. Setelah itu, setiap insiden produksi akan lebih cepat dipahami karena tim tidak lagi menebak-nebak, melainkan menelusuri bukti operasional yang tersambung dari ujung ke ujung.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!