Pelajari propagasi konteks asinkron JavaScript dengan AsyncLocalStorage untuk pelacakan permintaan, kelanjutan, dan membangun aplikasi sisi server yang kuat.
Propagasi Konteks Asinkron JavaScript: Pelacakan Permintaan dan Kelanjutan dengan AsyncLocalStorage
Dalam pengembangan JavaScript sisi server modern, khususnya dengan Node.js, operasi asinkron ada di mana-mana. Mengelola status dan konteks di seluruh batasan asinkron ini bisa menjadi tantangan. Postingan blog ini mengeksplorasi konsep propagasi konteks asinkron, berfokus pada cara menggunakan AsyncLocalStorage untuk mencapai pelacakan permintaan dan kelanjutan secara efektif. Kami akan memeriksa manfaat, batasan, dan aplikasi di dunia nyata, serta memberikan contoh praktis untuk mengilustrasikan penggunaannya.
Memahami Propagasi Konteks Asinkron
Propagasi konteks asinkron mengacu pada kemampuan untuk mempertahankan dan menyebarkan informasi konteks (misalnya, ID permintaan, detail autentikasi pengguna, ID korelasi) di seluruh operasi asinkron. Tanpa propagasi konteks yang tepat, akan sulit untuk melacak permintaan, menghubungkan log, dan mendiagnosis masalah kinerja dalam sistem terdistribusi.
Pendekatan tradisional untuk mengelola konteks sering kali mengandalkan pengiriman objek konteks secara eksplisit melalui pemanggilan fungsi, yang dapat menyebabkan kode menjadi bertele-tele dan rawan kesalahan. AsyncLocalStorage menawarkan solusi yang lebih elegan dengan menyediakan cara untuk menyimpan dan mengambil data konteks dalam satu konteks eksekusi, bahkan di seluruh operasi asinkron.
Memperkenalkan AsyncLocalStorage
AsyncLocalStorage adalah modul bawaan Node.js (tersedia sejak Node.js v14.5.0) yang menyediakan cara untuk menyimpan data yang bersifat lokal selama masa hidup operasi asinkron. Ini pada dasarnya menciptakan ruang penyimpanan yang dipertahankan di seluruh panggilan await, promise, dan batasan asinkron lainnya. Hal ini memungkinkan pengembang untuk mengakses dan memodifikasi data konteks tanpa harus mengirimkannya secara eksplisit.
Fitur utama AsyncLocalStorage:
- Propagasi Konteks Otomatis: Nilai yang disimpan di
AsyncLocalStoragesecara otomatis disebarkan ke seluruh operasi asinkron dalam konteks eksekusi yang sama. - Kode yang Disederhanakan: Mengurangi kebutuhan untuk mengirim objek konteks secara eksplisit melalui pemanggilan fungsi.
- Observabilitas yang Ditingkatkan: Memfasilitasi pelacakan permintaan serta korelasi log dan metrik.
- Aman untuk Thread (Thread-Safety): Menyediakan akses yang aman untuk thread ke data konteks dalam konteks eksekusi saat ini.
Kasus Penggunaan untuk AsyncLocalStorage
AsyncLocalStorage sangat berharga dalam berbagai skenario, termasuk:
- Pelacakan Permintaan: Menetapkan ID unik untuk setiap permintaan yang masuk dan menyebarkannya ke seluruh siklus hidup permintaan untuk tujuan pelacakan.
- Autentikasi dan Otorisasi: Menyimpan detail autentikasi pengguna (misalnya, ID pengguna, peran, izin) untuk mengakses sumber daya yang dilindungi.
- Pencatatan Log dan Audit: Melampirkan metadata spesifik permintaan ke pesan log untuk proses debug dan audit yang lebih baik.
- Pemantauan Kinerja: Melacak waktu eksekusi komponen yang berbeda dalam suatu permintaan untuk analisis kinerja.
- Manajemen Transaksi: Mengelola status transaksional di beberapa operasi asinkron (misalnya, transaksi basis data).
Contoh Praktis: Pelacakan Permintaan dengan AsyncLocalStorage
Mari kita ilustrasikan cara menggunakan AsyncLocalStorage untuk pelacakan permintaan dalam aplikasi Node.js sederhana. Kita akan membuat middleware yang menetapkan ID unik untuk setiap permintaan yang masuk dan menyediakannya di seluruh siklus hidup permintaan.
Contoh Kode
Pertama, instal paket yang diperlukan (jika perlu):
npm install uuid express
Berikut kodenya:
// app.js
const express = require('express');
const { AsyncLocalStorage } = require('async_hooks');
const { v4: uuidv4 } = require('uuid');
const app = express();
const asyncLocalStorage = new AsyncLocalStorage();
const port = 3000;
// Middleware untuk menetapkan ID permintaan dan menyimpannya di AsyncLocalStorage
app.use((req, res, next) => {
const requestId = uuidv4();
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('requestId', requestId);
next();
});
});
// Mensimulasikan operasi asinkron
async function doSomethingAsync() {
return new Promise(resolve => {
setTimeout(() => {
const requestId = asyncLocalStorage.getStore().get('requestId');
console.log(`[Async] Request ID: ${requestId}`);
resolve();
}, 50);
});
}
// Penangan rute (Route handler)
app.get('/', async (req, res) => {
const requestId = asyncLocalStorage.getStore().get('requestId');
console.log(`[Route] Request ID: ${requestId}`);
await doSomethingAsync();
res.send(`Hello World! Request ID: ${requestId}`);
});
app.listen(port, () => {
console.log(`App listening at http://localhost:${port}`);
});
Dalam contoh ini:
- Kita membuat sebuah instance
AsyncLocalStorage. - Kita mendefinisikan sebuah middleware yang menetapkan ID unik untuk setiap permintaan yang masuk menggunakan pustaka
uuid. - Kita menggunakan
asyncLocalStorage.run()untuk menjalankan penangan permintaan dalam konteksAsyncLocalStorage. Ini memastikan bahwa setiap nilai yang disimpan diAsyncLocalStoragetersedia di seluruh siklus hidup permintaan. - Di dalam middleware, kita menyimpan ID permintaan di
AsyncLocalStoragemenggunakanasyncLocalStorage.getStore().set('requestId', requestId). - Kita mendefinisikan fungsi asinkron
doSomethingAsync()yang mensimulasikan operasi asinkron dan mengambil ID permintaan dariAsyncLocalStorage. - Di dalam penangan rute, kita mengambil ID permintaan dari
AsyncLocalStoragedan menyertakannya dalam respons.
Saat Anda menjalankan aplikasi ini dan mengirim permintaan ke http://localhost:3000, Anda akan melihat ID permintaan dicatat di log baik di penangan rute maupun di fungsi asinkron, yang menunjukkan bahwa konteks telah disebarkan dengan benar.
Penjelasan
- Instance
AsyncLocalStorage: Kita membuat sebuah instanceAsyncLocalStorageyang akan menyimpan data konteks kita. - Middleware: Middleware mencegat setiap permintaan yang masuk. Ini menghasilkan UUID dan kemudian menggunakan
asyncLocalStorage.rununtuk menjalankan sisa alur penanganan permintaan *di dalam* konteks penyimpanan ini. Ini sangat penting; ini memastikan bahwa apa pun di hilir memiliki akses ke data yang disimpan. asyncLocalStorage.run(new Map(), ...): Metode ini mengambil dua argumen: sebuahMapbaru yang kosong (Anda dapat menggunakan struktur data lain jika sesuai untuk konteks Anda) dan sebuah fungsi callback. Fungsi callback berisi kode yang harus dieksekusi dalam konteks asinkron. Setiap operasi asinkron yang dimulai dalam callback ini akan secara otomatis mewarisi data yang disimpan dalamMap.asyncLocalStorage.getStore(): Ini mengembalikanMapyang diteruskan keasyncLocalStorage.run. Kita menggunakannya untuk menyimpan dan mengambil ID permintaan. Jikarunbelum dipanggil, ini akan mengembalikanundefined, itulah mengapa penting untuk memanggilrundi dalam middleware.- Fungsi Asinkron: Fungsi
doSomethingAsyncmensimulasikan operasi asinkron. Yang terpenting, meskipun asinkron (menggunakansetTimeout), fungsi ini masih memiliki akses ke ID permintaan karena berjalan dalam konteks yang dibentuk olehasyncLocalStorage.run.
Penggunaan Lanjutan: Menggabungkan dengan Pustaka Logging
Mengintegrasikan AsyncLocalStorage dengan pustaka logging (seperti Winston atau Pino) dapat secara signifikan meningkatkan observabilitas aplikasi Anda. Dengan menyuntikkan data konteks (misalnya, ID permintaan, ID pengguna) ke dalam pesan log, Anda dapat dengan mudah menghubungkan log dan melacak permintaan di berbagai komponen.
Contoh dengan Winston
// logger.js
const winston = require('winston');
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message }) => {
const requestId = asyncLocalStorage.getStore() ? asyncLocalStorage.getStore().get('requestId') : 'N/A';
return `${timestamp} [${level}] [${requestId}] ${message}`;
})
),
transports: [
new winston.transports.Console()
]
});
module.exports = {
logger,
asyncLocalStorage
};
// app.js (dimodifikasi)
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { logger, asyncLocalStorage } = require('./logger');
const app = express();
const port = 3000;
app.use((req, res, next) => {
const requestId = uuidv4();
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('requestId', requestId);
logger.info(`Incoming request: ${req.url}`); // Catat permintaan yang masuk
next();
});
});
async function doSomethingAsync() {
return new Promise(resolve => {
setTimeout(() => {
logger.info('Doing something async...');
resolve();
}, 50);
});
}
app.get('/', async (req, res) => {
logger.info('Handling request...');
await doSomethingAsync();
res.send('Hello World!');
});
app.listen(port, () => {
logger.info(`App listening at http://localhost:${port}`);
});
Dalam contoh ini:
- Kita membuat instance logger Winston dan mengonfigurasinya untuk menyertakan ID permintaan dari
AsyncLocalStoragedi setiap pesan log. Bagian kuncinya adalahwinston.format.printf, yang mengambil ID permintaan (jika tersedia) dariAsyncLocalStorage. Kita memeriksa apakahasyncLocalStorage.getStore()ada untuk menghindari kesalahan saat melakukan logging di luar konteks permintaan. - Kita memperbarui middleware untuk mencatat URL permintaan yang masuk.
- Kita memperbarui penangan rute dan fungsi asinkron untuk mencatat pesan menggunakan logger yang telah dikonfigurasi.
Sekarang, semua pesan log akan menyertakan ID permintaan, sehingga lebih mudah untuk melacak permintaan dan menghubungkan log.
Pendekatan Alternatif: cls-hooked dan Async Hooks
Sebelum AsyncLocalStorage tersedia, pustaka seperti cls-hooked umum digunakan untuk propagasi konteks asinkron. cls-hooked menggunakan Async Hooks (API Node.js tingkat lebih rendah) untuk mencapai fungsionalitas serupa. Meskipun cls-hooked masih banyak digunakan, AsyncLocalStorage umumnya lebih disukai karena sifatnya yang bawaan dan kinerjanya yang lebih baik.
Async Hooks (async_hooks)
Async Hooks menyediakan API tingkat lebih rendah untuk melacak siklus hidup operasi asinkron. Meskipun AsyncLocalStorage dibangun di atas Async Hooks, penggunaan Async Hooks secara langsung sering kali lebih kompleks dan kurang berkinerja. Async Hooks lebih sesuai untuk kasus penggunaan yang sangat spesifik dan canggih di mana kontrol terperinci atas siklus hidup asinkron diperlukan. Hindari menggunakan Async Hooks secara langsung kecuali benar-benar diperlukan.
Mengapa lebih memilih AsyncLocalStorage daripada cls-hooked?
- Bawaan (Built-in):
AsyncLocalStorageadalah bagian dari inti Node.js, menghilangkan kebutuhan akan dependensi eksternal. - Kinerja:
AsyncLocalStorageumumnya lebih berkinerja daripadacls-hookedkarena implementasinya yang dioptimalkan. - Pemeliharaan: Sebagai modul bawaan,
AsyncLocalStoragesecara aktif dipelihara oleh tim inti Node.js.
Pertimbangan dan Batasan
Meskipun AsyncLocalStorage adalah alat yang kuat, penting untuk menyadari batasannya:
- Batasan Konteks:
AsyncLocalStoragehanya menyebarkan konteks dalam konteks eksekusi yang sama. Jika Anda meneruskan data antar proses atau server yang berbeda (misalnya, melalui antrean pesan atau gRPC), Anda masih perlu melakukan serialisasi dan deserialisasi data konteks secara eksplisit. - Kebocoran Memori: Penggunaan
AsyncLocalStorageyang tidak tepat berpotensi menyebabkan kebocoran memori jika data konteks tidak dibersihkan dengan benar. Pastikan Anda menggunakanasyncLocalStorage.run()dengan benar dan hindari menyimpan data dalam jumlah besar diAsyncLocalStorage. - Kompleksitas: Meskipun
AsyncLocalStoragemenyederhanakan propagasi konteks, ia juga dapat menambah kompleksitas pada kode Anda jika tidak digunakan dengan hati-hati. Pastikan tim Anda memahami cara kerjanya dan mengikuti praktik terbaik. - Bukan Pengganti Variabel Global:
AsyncLocalStorage*bukan* pengganti untuk variabel global. Ini dirancang khusus untuk menyebarkan konteks dalam satu permintaan atau transaksi. Penggunaannya yang berlebihan dapat menyebabkan kode yang terikat erat dan membuat pengujian menjadi lebih sulit.
Praktik Terbaik untuk Menggunakan AsyncLocalStorage
Untuk menggunakan AsyncLocalStorage secara efektif, pertimbangkan praktik terbaik berikut:
- Gunakan Middleware: Gunakan middleware untuk menginisialisasi
AsyncLocalStoragedan menyimpan data konteks di awal setiap permintaan. - Simpan Data Minimal: Hanya simpan data konteks yang penting di
AsyncLocalStorageuntuk meminimalkan overhead memori. Hindari menyimpan objek besar atau informasi sensitif. - Hindari Akses Langsung: Enkapsulasi akses ke
AsyncLocalStoragedi balik API yang terdefinisi dengan baik untuk menghindari keterikatan yang erat dan meningkatkan kemudahan pemeliharaan kode. Buat fungsi atau kelas pembantu untuk mengelola data konteks. - Pertimbangkan Penanganan Kesalahan: Terapkan penanganan kesalahan untuk menangani kasus-kasus di mana
AsyncLocalStoragetidak diinisialisasi dengan benar secara baik. - Uji Secara Menyeluruh: Tulis pengujian unit dan integrasi untuk memastikan bahwa propagasi konteks berfungsi seperti yang diharapkan.
- Dokumentasikan Penggunaan: Dokumentasikan dengan jelas bagaimana
AsyncLocalStoragedigunakan dalam aplikasi Anda untuk membantu pengembang lain memahami mekanisme propagasi konteks.
Integrasi dengan OpenTelemetry
OpenTelemetry adalah kerangka kerja observabilitas sumber terbuka yang menyediakan API, SDK, dan alat untuk mengumpulkan dan mengekspor data telemetri (misalnya, jejak, metrik, log). AsyncLocalStorage dapat diintegrasikan dengan mulus dengan OpenTelemetry untuk menyebarkan konteks jejak secara otomatis di seluruh operasi asinkron.
OpenTelemetry sangat bergantung pada propagasi konteks untuk menghubungkan jejak di berbagai layanan. Dengan menggunakan AsyncLocalStorage, Anda dapat memastikan bahwa konteks jejak disebarkan dengan benar di dalam aplikasi Node.js Anda, memungkinkan Anda membangun sistem pelacakan terdistribusi yang komprehensif.
Banyak SDK OpenTelemetry secara otomatis menggunakan AsyncLocalStorage (atau cls-hooked jika AsyncLocalStorage tidak tersedia) untuk propagasi konteks. Periksa dokumentasi SDK OpenTelemetry pilihan Anda untuk detail spesifik.
Kesimpulan
AsyncLocalStorage adalah alat yang berharga untuk mengelola propagasi konteks asinkron dalam aplikasi JavaScript sisi server. Dengan menggunakannya untuk pelacakan permintaan, autentikasi, logging, dan kasus penggunaan lainnya, Anda dapat membangun aplikasi yang lebih kuat, dapat diamati, dan mudah dipelihara. Meskipun ada alternatif seperti cls-hooked dan Async Hooks, AsyncLocalStorage umumnya menjadi pilihan yang lebih disukai karena sifatnya yang bawaan, kinerja, dan kemudahan penggunaannya. Ingatlah untuk mengikuti praktik terbaik dan memperhatikan batasannya untuk memanfaatkan kemampuannya secara efektif. Kemampuan untuk melacak permintaan dan menghubungkan peristiwa di seluruh operasi asinkron sangat penting untuk membangun sistem yang skalabel dan andal, terutama dalam arsitektur layanan mikro dan lingkungan terdistribusi yang kompleks. Menggunakan AsyncLocalStorage membantu mencapai tujuan ini, yang pada akhirnya mengarah pada proses debug, pemantauan kinerja, dan kesehatan aplikasi secara keseluruhan yang lebih baik.