এই পর্বে যা শিখবেন
- Express 5-এ
pinoআরpino-httpবসানো, আসল log লাইন পড়া AsyncLocalStorageদিয়ে একটি request-এর সব লাইন এক id-তে বাঁধা- কী কখনো log করবেন না, আর
redact-এর সীমা কোথায় node --env-file, Zod দিয়ে config যাচাই আর শুরুতেই থেমে যাওয়া- Liveness, readiness, database check, graceful shutdown আর প্রাথমিক metrics
১ · console.log কোথায় ফুরিয়ে যায়
শুক্রবার সন্ধ্যা। “বাজারঘর”-এ অফার চলছে। একজন ক্রেতা ফেসবুকে লিখলেন, “টাকা কেটেছে, order আসেনি।” আপনি server-এর log খুললেন। সেখানে আছে:
here
order ok
here
Error: failed
order ok
user 42
here
কোন “here” কার? “failed” কোন order-এর? ঘটনা কখন ঘটেছিল? এই লাইনগুলো লেখার সময় আপনার মনে পুরো প্রসঙ্গ ছিল, কিন্তু log-এ সেই প্রসঙ্গ নেই। লেখার মানুষ আর পড়ার মুহূর্তের মধ্যে কয়েক মাসের দূরত্ব থাকতে পারে। console.log ছোট কাজের জন্য ভালো, কিন্তু production-এ তার তিনটি অভাব ধরা পড়ে:
- গঠন নেই। লেখা বাক্য যন্ত্র দিয়ে খোঁজা কঠিন। “৫০০ টাকার বেশি যেসব order ব্যর্থ হয়েছে” এই প্রশ্নের উত্তর এমন log থেকে আসে না।
- গুরুত্বের মাত্রা নেই। সাধারণ খবর আর জরুরি ভুল একই চেহারায় আসে, আর production-এ বাড়তি লাইন বন্ধ করার সহজ উপায় নেই।
- সংযোগ নেই। একসাথে একশো request চললে তাদের লাইন মিশে যায়। কোন লাইন কোন request-এর, বোঝার উপায় থাকে না।
পাড়ার চায়ের দোকানের খাতায় লেখা থাকে “রহিম ২০, চিনি কম, কাল দেবে”। মালিক নিজে পড়তে পারেন, কিন্তু মাস শেষে মোট বাকি বের করা কঠিন। ব্যাংকের লেনদেনের খাতা অন্যরকম: তারিখ, হিসাব নম্বর, ধরন, টাকা, প্রতিটি আলাদা ঘরে। তাই যে কেউ যেকোনো ঘর ধরে খুঁজতে আর যোগ করতে পারেন। Structured logging মানে log-কে দোকানের খাতা থেকে ব্যাংকের খাতায় তুলে আনা।
প্রচলিত আকার হলো প্রতি লাইনে একটি JSON object. আর অ্যাপ লেখে শুধু stdout-এ। Log ফাইল ঘোরানো, জমা করা, এক জায়গায় পাঠানো, এসব চালানোর পরিবেশের কাজ। Twelve-Factor App নীতিও log-কে ঘটনার একটি ধারা হিসেবে দেখে, যা অ্যাপ stdout-এ ছেড়ে দেয়।
| Level | কখন | বাজারঘরের উদাহরণ | Production-এ |
|---|---|---|---|
debug | কোডের ভেতরের খুঁটিনাটি | query শেষ হলো | সাধারণত বন্ধ |
info | স্বাভাবিক গুরুত্বপূর্ণ ঘটনা | order হয়েছে, server চালু | চালু |
warn | অস্বাভাবিক, কিন্তু সামলে নেওয়া গেছে | order পাওয়া যায়নি, দ্বিতীয় চেষ্টায় সফল | চালু |
error | কাজ ব্যর্থ, কারও দেখা দরকার | payment-এর উত্তর আসেনি | চালু, alert-এর উৎস |
এবার ৩০ লাইনের কম কোডে নিজেই একটি logger বানিয়ে ভেতরের কৌশলটি দেখুন। সংবেদনশীল key ঢাকার একটি ছোট ব্যবস্থাও এতে আছে। সত্যিকারের সময়ের বদলে এখানে ক্রমিক নম্বর, যাতে আউটপুট প্রতিবার এক থাকে।
const LEVELS = { debug: 20, info: 30, warn: 40, error: 50 };
const SENSITIVE = new Set(["password", "token", "phone", "cardNumber"]);
// JSON.stringify প্রতিটি key-তে, প্রতিটি গভীরতায়, এই ফাংশন ডাকে
function hideSensitive(key, value) {
return SENSITIVE.has(key) ? "[REDACTED]" : value;
}
function createLogger(minLevel) {
let seq = 0; // নিশ্চিত আউটপুটের জন্য সময়ের বদলে ক্রমিক নম্বর
function write(level, fields, msg) {
if (LEVELS[level] < LEVELS[minLevel]) return;
const line = { level, seq: ++seq, ...fields, msg };
console.log(JSON.stringify(line, hideSensitive));
}
return {
debug: (fields, msg) => write("debug", fields, msg),
info: (fields, msg) => write("info", fields, msg),
error: (fields, msg) => write("error", fields, msg),
};
}
const log = createLogger("info");
log.debug({ sql: "SELECT ..." }, "query text");
log.info({ orderId: 42, totalTaka: 1250 }, "order placed");
log.info({ user: { phone: "01700000000", city: "Rajshahi" } }, "signup");
log.error({ orderId: 42, token: "example-token" }, "payment failed");চারটি কলের জায়গায় তিনটি লাইন এসেছে, কারণ debug-এর মান 20, আর ন্যূনতম level info-র মান 30. প্রথম লাইন: {"level":"info","seq":1,"orderId":42,"totalTaka":1250,"msg":"order placed"}. দ্বিতীয় লাইনে ফোন নম্বর ভেতরের object-এ ছিল, তবু দেখা যাচ্ছে "phone":"[REDACTED]", কারণ JSON.stringify-এর replacer ফাংশন প্রতিটি গভীরতায় চলে। শেষ লাইনে "token":"[REDACTED]". আর সবখানেই orderId একটি আলাদা ঘর, তাই যন্ত্র দিয়ে খোঁজা সহজ।
হিসাব: log-এর আয়তন কত হয়
Level শুধু পরিষ্কার পড়ার জন্য নয়, খরচের জন্যও জরুরি। ধরে নিন, দিনে ২,০০,০০০ request, প্রতিটিতে ৪টি লাইন, প্রতি লাইনে গড়ে ২০০ byte. আমাদের ল্যাবের ২৩টি লাইনের গড় ছিল প্রায় ১৬৪ byte, তবে এটি একটি ছোট লোকাল run; বাস্তবে লাইন বড় হয়।
debug চালু, ৭ লাইন: 2,00,000 × 7 × 200 B = 280 MB প্রতিদিন → 8.4 GB
Health check-ও লাইন বাড়ায়। দুটি load balancer ৫ সেকেন্ড পরপর ডাকলে দিনে 86,400 ÷ 5 × 2 = 34,560টি কল। প্রতিটির একটি লাইন মানে দিনে প্রায় ৬.৯ MB অপ্রয়োজনীয় লেখা। তাই নিচে এদের log থেকে বাদ দেওয়া হয়েছে।
২ · Express 5-এ pino আর pino-http
নিজের logger শেখার জন্য ভালো, কিন্তু production-এ পরীক্ষিত library ব্যবহার করুন। Node.js জগতে pino বহুল ব্যবহৃত, winston-ও অনেক প্রকল্পে চলে। pino-http হলো তার middleware, যা প্রতিটি request শেষে একটি লাইন লেখে। Middleware-এর ক্রম নিয়ে বিস্তারিত আছে পর্ব ২৬-এ।
লেখার সময়ের version: Node 24.15.0, Express 5.3.0, pino 10.4.0, pino-http 11.0.0, Zod 4.6.7. এগুলো বদলায়, তাই নিজের project-এ npm view pino version দিয়ে মিলিয়ে নিন।
npm i express pino pino-http zod prom-client
npm i -D pino-prettyimport pino from 'pino';
import { requestContext } from './context.js';
export function createLogger({ level }) {
return pino({
level,
base: null, // pid আর hostname বাদ
timestamp: pino.stdTimeFunctions.isoTime,
formatters: { level: (label) => ({ level: label }) },
mixin() {
const store = requestContext.getStore();
return store ? { requestId: store.requestId } : {};
},
redact: {
paths: [
'password', '*.password', 'token', '*.token',
'phone', '*.phone', 'cardNumber', '*.cardNumber',
'req.headers.authorization', 'req.headers.cookie',
],
censor: '[REDACTED]',
},
});
}লাইনগুলো ছোট করে বুঝি। pino নিজে থেকে প্রতিটি লাইনে pid আর hostname যোগ করে। base: null সেগুলো বাদ দেয়, তাই এই পর্বের সব আউটপুটে এই দুটি নেই। অনেক প্রকল্পে এগুলো রাখাই কাজের, কারণ কয়েকটি server-এর মধ্যে কোনটি থেকে লাইন এলো তা বোঝা যায়। isoTime সময়কে পড়ার মতো করে। formatters.level সংখ্যা 30-এর বদলে info লেখে। mixin প্রতিটি লাইনে একটি বাড়তি field জোড়ে; এর ভূমিকা পরের অংশে।
app.use(pinoHttp({
logger: log,
genReqId: (req) => req.id,
autoLogging: {
ignore: (req) => ['/healthz', '/readyz', '/metrics'].includes(req.url),
},
serializers: {
req: (r) => ({ method: r.method, url: r.url.split('?')[0] }),
res: (r) => ({ statusCode: r.statusCode }),
},
}));সত্যিকারের run. একটি GET /orders/42-এর শেষে pino-http এই লাইনটি লিখল (Node 24.15.0, ল্যাবের project ~/bazarghar):
{"level":"info","time":"2026-11-01T16:33:40.268Z","req":{"method":"GET","url":"/orders/42"},"requestId":"8e08e93d-e083-4d4c-8662-9c53e882a724","res":{"statusCode":200},"responseTime":25,"msg":"request completed"}
responseTime এখানে মিলিসেকেন্ডে, অর্থাৎ এই একটি লোকাল run-এ ২৫ ms. Development-এ JSON পড়া ক্লান্তিকর হলে আউটপুট pino-pretty-তে পাঠান। Logger কোড বদলায় না, শুধু পড়ার চেহারা বদলায়:
$ node --env-file=.env src/main.js | npx pino-pretty
[22:33:40] INFO: request completed
req: {
"method": "GET",
"url": "/orders/42"
}
requestId: "8e08e93d"
res: {
"statusCode": 200
}
responseTime: 25
আসল run-এ ধরা পড়া ফাঁদ। আমরা serializers কেন লিখলাম? pino-http-এর serializer না বদলালে request-এর সব header লেখা হয়। একটি পরীক্ষায় Authorization আর Cookie header পাঠিয়ে default log দেখা গেল (নকল মান, লাইনটি ছোট করা): "url":"/me?resetToken=example-reset" … "authorization":"Bearer example-token","cookie":"sid=example-session". অর্থাৎ login token, session আর URL-এর ভেতরের reset token, সবই log-এ! তাই শুধু method আর query বাদ দেওয়া url রাখা হয়েছে।
৩ · Request id — এক request-এর সব লাইন এক সুতোয়
এখন একসাথে দুটি order দেখা হচ্ছে। Async কোডে এদের লাইন মিশে যায়। পর্ব ২-এ request id-এর ধারণা পেয়েছিলেন (অনুরোধের যাত্রা)। প্রতিটি request-এর জন্য একটি আলাদা পরিচয়, যা তার প্রতিটি log লাইনে থাকবে।
ডাকঘরে একটি রেজিস্ট্রি চিঠি গ্রহণের সময় একটি নম্বর পায়। বাছাইঘর, বস্তা, পিয়ন, যে যেখানে খাতায় লেখেন, ওই নম্বরটিই লেখেন। হারিয়ে গেলে নম্বর ধরে পুরো পথ বের করা যায়। Request id ঠিক এই রেজিস্ট্রি নম্বর।
সমস্যা হলো, id-টি repository-এর গভীর ফাংশন পর্যন্ত পৌঁছাবে কীভাবে? প্রতিটি ফাংশনে requestId argument যোগ করলে কোড নোংরা হয়, আর একটি জায়গায় ভুলে গেলেই সুতো ছিঁড়ে যায়। Module-স্তরের একটি সাধারণ variable-ও চলবে না, কারণ একই process-এ অনেক request একসাথে চলে; একটি অন্যটির মান মুছে দেবে।
সমাধান node:async_hooks-এর AsyncLocalStorage. run(store, fn) একটি অদৃশ্য বাক্স খোলে। fn-এর ভেতর থেকে শুরু হওয়া সব async কাজ, await, timer, যাই হোক, getStore() দিয়ে একই বাক্স পায়। Node-এর docs অনুযায়ী এটি v16.4.0 থেকে Stable. নিচের কোডে দুটি request একসাথে চলছে, আর node:http ছাড়া কোনো package নেই।
const http = require("node:http");
const { AsyncLocalStorage } = require("node:async_hooks");
const context = new AsyncLocalStorage();
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
function log(msg) {
const requestId = context.getStore()?.requestId ?? "none";
console.log(JSON.stringify({ requestId, msg }));
}
// খেয়াল করুন: এই দুই ফাংশনে requestId কোনো argument হয়ে আসেনি
async function findOrder(id, ms) {
await wait(ms);
log(`repository: order ${id} found`);
}
async function getOrder(id, ms) {
log(`service: loading order ${id}`);
await findOrder(id, ms);
}
let next = 0;
const server = http.createServer((req, res) => {
const [, id, ms] = req.url.split("/");
context.run({ requestId: `req-${++next}` }, async () => {
await getOrder(id, Number(ms));
log("response sent");
res.end("ok");
});
});
server.listen(0, "127.0.0.1", async () => {
const url = `http://127.0.0.1:${server.address().port}`;
const first = fetch(`${url}/42/80`);
await wait(20);
await Promise.all([first, fetch(`${url}/7/20`)]);
log("outside any request");
server.close();
});Order 42-এর কাজে 80 ms লাগে, আর order 7 শুরু হয় 20 ms পরে, তার লাগে 20 ms. তাই ছোট কাজটি আগে শেষ হয়, আর লাইনগুলো মিশে যায়:
{"requestId":"req-1","msg":"service: loading order 42"}
{"requestId":"req-2","msg":"service: loading order 7"}
{"requestId":"req-2","msg":"repository: order 7 found"}
{"requestId":"req-2","msg":"response sent"}
{"requestId":"req-1","msg":"repository: order 42 found"}
{"requestId":"req-1","msg":"response sent"}
{"requestId":"none","msg":"outside any request"}
মিশে গেলেও প্রতিটি লাইন সঠিক id পেয়েছে, যদিও findOrder id জানে না। শেষ লাইনে id নেই, কারণ সেটি run()-এর বাইরে; সেখানে getStore() দেয় undefined, আর কোড লেখে none.
Express-এ একই ধারণা একটি middleware হয়ে যায়। এটি সবার আগে বসে, যাতে পরের সব middleware আর route বাক্সের ভেতরে চলে। বাকি কাজ করে আগে দেখা pino mixin: logger প্রতিটি লাইন লেখার সময় নিজেই getStore() ডাকে।
const SAFE_ID = /^[A-Za-z0-9-]{8,64}$/;
app.use((req, res, next) => {
const incoming = req.get('x-request-id');
const requestId = incoming && SAFE_ID.test(incoming)
? incoming
: randomUUID();
req.id = requestId;
res.setHeader('x-request-id', requestId);
requestContext.run({ requestId }, next);
});এখন দেখুন, এই id স্তরগুলো পেরিয়ে কীভাবে যায়। প্রতিটি log লাইন ল্যাবের আসল run থেকে নেওয়া; পড়ার সুবিধায় id ছোট করা হয়েছে। Service আর repository-এর গঠন পর্ব ২৯-এর আলোচ্য।
- Middleware
run({ requestId })id তৈরি - Service
getOrder(42)argument-এ id নেই - Repository
find(42)await-এর পরেও আছে - pino-http
res finishশেষের লাইন
x-request-id: 8e08e93d… store = { requestId: "8e08e93d…" }
{"level":"info","requestId":"8e08e93d…",
"orderId":42,"msg":"loading order"}
{"level":"debug","requestId":"8e08e93d…",
"table":"orders","found":true,"msg":"query done"}
{"level":"info","requestId":"8e08e93d…",
"res":{"statusCode":200},"responseTime":25,
"msg":"request completed"}
$ grep -c . app.log
74
$ grep 8e08e93d app.log | wc -l
3
- দরজায় নম্বর। Request এলো। Middleware একটি
randomUUID()বানাল, response header-এ বসাল, আরrun()দিয়ে বাক্স খুলল। ক্রেতার কাছেও এই id গেল, তাই সমস্যা হলে তিনি সেটি জানাতে পারেন। - Service-এ প্রথম লাইন।
getOrder(42)শুধু order id পেয়েছে।log.info()ডাকতেইmixinবাক্স থেকেrequestIdএনে লাইনে জুড়ে দিল। - Repository-তে দ্বিতীয় লাইন। মাঝে
awaitহয়েছে, এই ফাঁকে অন্য request-ও চলেছে। তবুgetStore()সঠিক বাক্সই দিল। সাধারণ global variable হলে এখানেই ভুল id আসত। - শেষের লাইন। Response পাঠানো শেষ। pino-http status 200 আর সময় 25 ms লিখল, একই id-সহ। এই একটি লাইন থেকেই request-এর ফল বোঝা যায়।
- পরে খোঁজা। ল্যাবের মিশ্র আউটপুটে ৭৪টি লাইন ছিল। ক্রেতা id জানালে
grep-এ শুধু ওই request-এর ৩টি লাইন আসে। পুরো গল্প ক্রমসহ হাতের মুঠোয়।
বাইরে থেকে আসা x-request-id অন্ধভাবে বিশ্বাস করবেন না। এটি ব্যবহারকারীর ইনপুট; এতে ভুয়া log লাইন বানানোর লেখা বা বিশাল string থাকতে পারে। তাই SAFE_ID regex দিয়ে যাচাই। সত্যিকারের run-এ bad id {"level":"fake"} পাঠালে সেটি বাদ পড়ে নতুন UUID তৈরি হলো, আর phone-app-7f3a9c গৃহীত হলো। ইনপুট যাচাইয়ের সাধারণ নিয়ম পর্ব ২৭-এ।
৪ · কী কখনো log করবেন না
Log সাধারণত database-এর চেয়ে কম সুরক্ষিত থাকে। অনেক মানুষ দেখতে পারেন, অন্য সিস্টেমে কপি হয়, মাসের পর মাস থাকে। তাই যা database-এ hash করে রাখেন, তা log-এ খোলা লিখলে সব সুরক্ষা অর্থহীন। তাত্ত্বিক আলোচনা আছে Cybersecurity কোর্সের Security logging পর্বে; এখানে শুধু হাতের কাজ।
কখনো নয়
Password, OTP, session id, access বা reset token, API key, card নম্বর, CVV, mobile banking PIN, সম্পূর্ণ request body.
ফাঁস হলেই সরাসরি ক্ষতিন্যূনতম, সাবধানে
ফোন নম্বর, ইমেইল, ঠিকানার মতো PII. দরকার হলে ঢাকা অংশ, যেমন শেষ ৩টি সংখ্যা। অনেক সময় অভ্যন্তরীণ userId-ই যথেষ্ট।
নিশ্চিন্তে
requestId, orderId, route, status code, সময়, ভুলের ধরন।
pino-র redact নির্দিষ্ট path-এর মান লেখার আগেই বদলে দেয়। Path-এ * মানে এক স্তরের যেকোনো key. একটি login-এ পুরো body log করে পরীক্ষা করা হলো (ইচ্ছা করেই খারাপ অভ্যাস, নকল মান):
{"level":"info","time":"2026-11-01T16:33:40.370Z","requestId":"65c68c0f-3467-4bc9-9754-bba00c7e4808","body":{"phone":"[REDACTED]","password":"[REDACTED]","profile":{"password":"example-2"}},"msg":"login attempt"}
লাইনটি মন দিয়ে দেখুন। body.password ঢাকা পড়েছে, কিন্তু body.profile.password খোলা! *.password শুধু এক স্তর নিচে মেলে, যেকোনো গভীরতায় নয়। আমাদের ছোট logger-এর replacer প্রতিটি গভীরতায় কাজ করত; pino-র path সেভাবে করে না। Node-এর জগতে এই পার্থক্য মনে রাখা জরুরি: redact শুধু আপনার লেখা path চেনে। তাই প্রথম নিয়ম হলো পুরো object log না করা:
// ভুল: কী কী আসবে, আপনি জানেন না
log.info({ body: req.body }, 'login attempt');
// ঠিক: নিজের বেছে নেওয়া field
log.info({ userId: user.id, method: 'password' }, 'login succeeded');এই “বেছে নেওয়া field”-এর নীতিকে allowlist বলা হয়। redact হলো তার পেছনের দ্বিতীয় বেড়া, যদি কেউ ভুল করে। আরেকটি সতর্কতা: pino-র docs অনুযায়ী redact path শুরুতে একবার ঠিক করার জিনিস, ব্যবহারকারীর ইনপুট থেকে কখনো আসবে না। আর wildcard path সরাসরি লেখা path-এর চেয়ে বেশি খরচের।
৫ · Config — কোডে নয়, environment-এ
একই কোড আপনার ল্যাপটপে, test-এ আর production-এ চলে, কিন্তু database আলাদা, port আলাদা, log level আলাদা। যা deploy ভেদে বদলায়, সেটিই config. Twelve-Factor App-এর নীতি: config কোড থেকে কঠোরভাবে আলাদা থাকবে, আর থাকবে environment variable-এ। এর একটি সহজ পরীক্ষাও আছে: কোনো credential ফাঁস না করে এখনই কি codebase প্রকাশ করা যায়?
দর্জির কাছে একটি শার্টের নকশা একটাই, কিন্তু মাপ প্রতিজন ক্রেতার আলাদা। মাপ নকশার ভেতরে সেলাই করে দিলে অন্য কারও জন্য আর চলবে না। কোড হলো নকশা, config হলো মাপের খাতা। একই build বিভিন্ন জায়গায় চলে, শুধু মাপ বদলায়।
Vite-এর .env আর VITE_ prefix দেখেছিলেন পর্ব ৫-এ; সেটি browser-এর গল্প। Server-এ .env ফাইল শুধু লোকাল সুবিধার জন্য, আর Node এটি নিজেই পড়তে পারে। --env-file এসেছে v20.6.0-তে, আর docs অনুযায়ী v24.10.0 ও v22.21.0 থেকে এটি আর experimental নয়। ছোট প্রকল্পে এখন dotenv package প্রায়ই লাগে না।
# নকল মান, শুধু লোকাল কাজের জন্য
NODE_ENV=development
PORT=4131
LOG_LEVEL=debug
DATABASE_PATH=./dev.sqlite
SESSION_SECRET=example-secret-not-real-0123456789abcdefঅগ্রাধিকারের নিয়ম সত্যিকারের run-এ যাচাই করা হয়েছে (Node 24.15.0; .env-এ PORT=4131, LOG_LEVEL=debug, আর .env.test-এ LOG_LEVEL=warn):
$ PORT=4140 node --env-file=.env print.js
PORT = 4140 | LOG_LEVEL = debug
$ node --env-file=.env --env-file=.env.test print-level.js
LOG_LEVEL = warn
$ node --env-file=.env.nope src/main.js
node: .env.nope: not found
exit code: 9
$ node --env-file-if-exists=.env.nope src/main.js
.env.nope not found. Continuing without it.
তিনটি নিয়ম বেরিয়ে এলো। Environment-এ আগে থেকে থাকা মান ফাইলের ওপরে জেতে। একাধিক ফাইলে পরের ফাইল আগেরটিকে ছাপিয়ে যায়। আর ফাইল না থাকলে --env-file থেমে যায়, কিন্তু --env-file-if-exists এগিয়ে যায়। প্রথম নিয়মটি production-এ কাজে লাগে: চালানোর প্ল্যাটফর্ম যে মান দেয়, সেটিই আসল।
Secret রেপোর বাইরে
Secret মানে যে মান ফাঁস হলে ক্ষতি। কোড একবার repository-তে গেলে তার ইতিহাসে থেকে যায়, পরের commit-এ মুছলেও। তাই শুরু থেকেই:
.env
.env.*
!.env.example# এই ফাইল commit হয়: নাম থাকে, আসল মান থাকে না
NODE_ENV=development
PORT=4131
LOG_LEVEL=info
DATABASE_PATH=
SESSION_SECRET=মনে রাখবেন, .gitignore আগেই commit হওয়া ফাইল সরায় না। Secret একবার ঠেলে দিলে ফাইল মুছে শান্তি নেই; মানটি ফাঁস ধরে নিয়ে নতুন secret বানিয়ে পুরোনোটি বাতিল করুন।
| বিষয় | development | test | production |
|---|---|---|---|
| মান কোথা থেকে | .env | .env.test বা CI | প্ল্যাটফর্ম বা secret manager |
LOG_LEVEL | debug | warn | info |
| Log-এর চেহারা | pino-pretty | কম আউটপুট | JSON, stdout-এ |
| Database | লোকাল | আলাদা test database | আসল, আলাদা credential |
| Secret | নকল মান | নকল মান | আসল, প্রতিটি পরিবেশে ভিন্ন |
Staging, secret manager আর প্রতিটি পরিবেশের পূর্ণ আলোচনা পর্ব ৭৮-এ আসবে। Cloud-এর দিক দেখতে পারেন Secrets ও KMS পর্বে।
৬ · শুরুতেই যাচাই — ভুল config-এ থেমে যান
ধরুন SESSION_SECRET দিতে ভুলে গেছেন। সাধারণ কোডে server দিব্যি চালু হবে, health check সবুজ, আর সমস্যা দেখা দেবে কয়েক ঘণ্টা পরে, প্রথম login-এ। তখন খুঁজে বের করা কঠিন। Fail fast নীতি বলে: ভুল config-এ server খোলাই যাবে না। Deploy সেখানেই ব্যর্থ হোক, যেখানে পুরোনো version এখনো চলছে।
Config আর deploy-এর ভুল কত দামি হতে পারে, তার বহুল আলোচিত উদাহরণ ২০১২ সালের Knight Capital ঘটনা। US SEC-এর প্রকাশিত প্রতিবেদন অনুযায়ী, আটটি server-এর একটিতে নতুন কোড পৌঁছায়নি, আর পুরোনো একটি flag নতুন অর্থে ব্যবহার করায় পুরোনো কোড জেগে ওঠে। প্রায় ৪৫ মিনিটে ক্ষতি হয় প্রায় ৪৬ কোটি ডলার। এক লাইনের যাচাই পুরো ঘটনা ঠেকাত, এমন দাবি করা যায় না; তবে শিক্ষাটি স্পষ্ট: পরিবেশ ঠিক আছে কি না, যন্ত্রকে দিয়েই যাচাই করান।
Environment variable-এর সব মান string. PORT সংখ্যা নয়, "4131". Zod-এর schema (পর্ব ২১-এর form validation আর পর্ব ২৭-এর request যাচাই থেকে পরিচিত) একই সাথে যাচাই করে আর ধরন বদলায়।
import { z } from 'zod';
const schema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
PORT: z.coerce.number().int().min(1).max(65535),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
DATABASE_PATH: z.string().min(1),
SESSION_SECRET: z.string().min(32),
SHUTDOWN_DRAIN_MS: z.coerce.number().int().min(0).default(1000),
});
export function loadConfig(source) {
const result = schema.safeParse(source);
if (!result.success) {
// শুধু কোন key-তে সমস্যা, মান কখনো নয়
const lines = result.error.issues.map(
(i) => ` ${i.path.join('.')}: ${i.message}`,
);
throw new Error(`Invalid config:\n${lines.join('\n')}`);
}
return Object.freeze(result.data);
}import { start } from './server.js';
try {
start(process.env);
} catch (err) {
console.error(err.message);
process.exit(1);
}loadConfig process.env নিজে পড়ে না; সে একটি object নেয়। ফলে test-এ সহজেই নকল object দেওয়া যায়। সত্যিকারের run (Zod 4.6.7), যেখানে .env-এর ওপরে shell থেকে দুটি ভুল মান বসানো হয়েছে:
$ PORT=abc SESSION_SECRET=short node --env-file=.env src/main.js
Invalid config:
PORT: Invalid input: expected number, received NaN
SESSION_SECRET: Too small: expected string to have >=32 characters
exit code: 1
দুটি ভুল একসাথে এসেছে, তাই একবারেই ঠিক করা যায়। আর বার্তায় short মানটি নেই, শুধু key-র নাম। নিচের ধাপগুলোয় ওই একই run ভেঙে দেখুন।
1node --env-file=.env src/main.js2const config = loadConfig(process.env);3// issues থাকলে: throw4const log = createLogger(config);5const db = openDb(config.DATABASE_PATH);6app.listen(config.PORT);7catch: process.exit(1);
| PORT | |
| SECRET | |
| database | |
| port | |
| exit code |
- মান জমা হলো। Node
.envপড়ল, কিন্তু shell-এ দেওয়াPORT=abcআরSESSION_SECRET=shortজিতল। এখন সবই string. কোনো যাচাই হয়নি। - PORT যাচাই।
z.coerce.number()"abc"-কে সংখ্যায় বদলাতে গিয়ে পেলNaN. একটি issue জমা হলো, কিন্তুsafeParseথামল না। - SESSION_SECRET যাচাই।
short-এর দৈর্ঘ্য 5, দরকার ন্যূনতম 32. দ্বিতীয় issue জমা হলো। বাকি field-গুলো.envথেকে ঠিক আছে। - Throw.
result.successfalse, তাইloadConfigerror ছুড়ল।createLogger,openDbআরlistenচলেইনি। - থেমে গেল।
catchবার্তা ছাপিয়ে exit code 1 দিল। Port কখনো খোলেনি, তাই একটি request-ও এই ভুল server-এ আসেনি। Deploy-এর ব্যবস্থা নতুন version বাতিল করতে পারে। - ঠিক করে আবার। ভুল মান দুটি সরালে
.env-এর মান ফিরে এলো:PORTসংখ্যা 4131, secret 40 অক্ষর। এবার database খুলল, port খুলল, log-এserver listening.
একই ধারণা package ছাড়া, নিজে চালিয়ে দেখুন। এখানে আসল environment ছোঁয়া হচ্ছে না; brokenEnv আর goodEnv তার জায়গায় বসানো দুটি সাধারণ নকল object.
// নকল environment: সত্যিকারের environment-এর জায়গায় একটি সাধারণ object
const brokenEnv = { NODE_ENV: "prod", PORT: "abc", SESSION_SECRET: "short" };
const goodEnv = {
NODE_ENV: "production",
PORT: "4131",
DATABASE_PATH: "./shop.sqlite",
SESSION_SECRET: "example-secret-not-real-0123456789abcdef",
};
const rules = {
NODE_ENV: (v) => ["development", "test", "production"].includes(v),
PORT: (v) => Number.isInteger(Number(v)) && Number(v) > 0 && Number(v) < 65536,
DATABASE_PATH: (v) => typeof v === "string" && v.length > 0,
SESSION_SECRET: (v) => typeof v === "string" && v.length >= 32,
};
function loadConfig(source) {
// একটি ভুলে না থেমে সব ভুল একসাথে জমা করা
const bad = Object.keys(rules).filter((key) => !rules[key](source[key]));
if (bad.length > 0) throw new Error(`Invalid config: ${bad.join(", ")}`);
return Object.freeze({ ...source, PORT: Number(source.PORT) });
}
function boot(name, source) {
console.log(`--- boot with ${name}`);
try {
const config = loadConfig(source);
console.log(`listening on port ${config.PORT} (${config.NODE_ENV})`);
} catch (err) {
console.log(`${err.message} -> exit code 1, no port opened`);
}
}
boot("brokenEnv", brokenEnv);
boot("goodEnv", goodEnv);ভাঙা object-এর ফল: Invalid config: NODE_ENV, PORT, DATABASE_PATH, SESSION_SECRET -> exit code 1, no port opened. চারটি ভুলই একসাথে এসেছে, এমনকি যে DATABASE_PATH object-এ নেই সেটিও। ভালো object দিলে: listening on port 4131 (production). Object.freeze-এর কারণে config চলার মাঝে কেউ বদলাতে পারে না।
একটি চুপচাপ ফাঁদ। Flag-এর মান সরাসরি boolean-এ coerce করবেন না। সত্যিকারের পরীক্ষায় z.coerce.boolean().parse('false') দিল true, কারণ খালি নয় এমন যেকোনো string JavaScript-এ truthy. Zod 4-এর z.stringbool() একই ইনপুটে false দিল। আর z.coerce.number() খালি string-কে 0 বানায়, তাই .min(1) রাখা জরুরি।
৭ · Health check — বেঁচে আছি, আর কাজ নিতে পারি
Server একা চলে না। বাইরের ব্যবস্থা নিয়মিত জানতে চায়, আপনি ঠিক আছেন তো? এই ছোট endpoint-ই health check. প্রশ্ন কিন্তু দুটি, আর তাদের উত্তরের ফলও আলাদা।
একজন রেস্তোরাঁর ওয়েটারকে দুটি প্রশ্ন করা যায়। “আপনি সুস্থ আছেন তো?” আর “এখন নতুন টেবিল নিতে পারবেন?” রান্নাঘরে গ্যাস ফুরিয়ে গেলে দ্বিতীয় উত্তর “না”, কিন্তু তাই বলে ওয়েটারকে হাসপাতালে পাঠালে গ্যাস ফিরে আসবে না।
দুই প্রশ্ন, দুই ফল
Readiness checks dependencies; liveness checks only the process
// Liveness: process বেঁচে আছে। বাইরের কিছু দেখে না।
app.get('/healthz', (req, res) => res.json({ status: 'alive' }));
// Readiness: এখন কি নতুন request নিতে পারি?
app.get('/readyz', (req, res) => {
if (state.shuttingDown) {
return res.status(503).json({ status: 'draining' });
}
try {
db.ping(); // SELECT 1
res.json({ status: 'ready', database: 'ok' });
} catch (err) {
log.warn({ reason: err.message }, 'readiness: database down');
res.status(503).json({ status: 'not-ready', database: 'down' });
}
});এই ল্যাবে ছোট database হিসেবে Node-এর node:sqlite ব্যবহার করা হয়েছে; Prisma আসবে পর্ব ৩৯-এ। সত্যিকারের run-এ চলন্ত অবস্থায় database বন্ধ করে দুটি endpoint ডাকা হলো:
/healthz 200 {"status":"alive"}
/readyz 200 {"status":"ready","database":"ok"}
── database বন্ধ করা হলো ──
/healthz 200 {"status":"alive"}
{"level":"warn","time":"2026-11-01T16:33:40.407Z","requestId":"28206835-2c2a-4a68-a4f8-e486be042198","reason":"database is not open","msg":"readiness: database down"}
/readyz 503 {"status":"not-ready","database":"down"}
| Liveness | Readiness | |
|---|---|---|
| প্রশ্ন | process আটকে যায়নি তো? | এখন নতুন request নিতে পারি? |
| যা দেখে | শুধু process নিজে | database, জরুরি সংযোগ, shutdown অবস্থা |
| ব্যর্থ হলে | process restart | traffic সরে যায়, process চলে |
| সাধারণ path | /healthz | /readyz |
Liveness-এ database check দেবেন না। Database এক মিনিট ধীর হলেই সব instance-এর liveness ব্যর্থ হবে, সব একসাথে restart হবে, আর ফিরে এসে সবাই একসাথে database-এ সংযোগ চাইবে। Database-এর ওপর চাপ আরও বাড়বে। একে বলে cascading failure. Kubernetes-এর docs-ও সতর্ক করে যে ভুল liveness probe এমন ধারাবাহিক ব্যর্থতা ঘটাতে পারে। Readiness-এর database check-ও হালকা রাখুন: SELECT 1, ছোট timeout, ভারী query নয়।
৮ · Graceful shutdown — দরজা বন্ধ, ভেতরের কাজ শেষ
নতুন version চালাতে পুরোনো process থামাতেই হয়। হঠাৎ থামালে কী হয়? সত্যিকারের run-এ /slow নামের ৮০০ ms-এর একটি কাজ চলাকালীন process-কে সরাসরি শেষ করা হলো (Windows-এ এই কাজটি শর্তহীন):
client error: ECONNRESET
child exit: code null signal SIGTERM
ক্রেতার request মাঝপথে কেটে গেল। Order লেখা হয়েছে কি না, কেউ জানে না। সমাধান graceful shutdown: দোকান বন্ধের সময় আগে শাটার অর্ধেক নামানো, নতুন ক্রেতা না নেওয়া, ভেতরের ক্রেতার কাজ শেষ করা, তারপর তালা।
Linux-এ platform প্রথমে SIGTERM পাঠায়, সময় শেষে SIGKILL, যা ধরা যায় না। ক্রমটিই আসল কথা। server.close() আগে ডাকলে load balancer তখনও জানে না, আর নতুন request গিয়ে ধাক্কা খায়। তাই আগে readiness 503, একটু অপেক্ষা, তারপর close. একেই বলে connection draining.
const shutdown = (signal) => {
if (state.shuttingDown) return;
state.shuttingDown = true; // এখন থেকে /readyz দেবে 503
log.info({ signal }, 'shutdown: readiness now 503');
const force = setTimeout(() => {
log.error('shutdown: timed out, forcing exit');
process.exit(1);
}, 10_000);
force.unref();
setTimeout(() => {
log.info('shutdown: closing server');
// চলমান request শেষে ফাঁকা হওয়া keep-alive সংযোগও বন্ধ করা
const sweep = setInterval(() => server.closeIdleConnections(), 100);
server.close(() => {
clearInterval(sweep);
db.close();
log.info('shutdown: clean exit');
clearTimeout(force);
process.exitCode = 0;
});
}, config.SHUTDOWN_DRAIN_MS);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);এবার ধাপে ধাপে দেখুন। সংখ্যাগুলো ল্যাবের একটি run থেকে: drain ৩০০ ms, আর /slow একটি request আগে থেকেই চলছে। এই run-এর সময় মেশিনভেদে বদলাবে। ল্যাবের এই run Windows-এ, তাই SIGTERM সেখানে আসল signal নয়, process.emit দিয়ে অনুকরণ করা। আসল signal পাঠানো Linux run এই পর্বে আলাদা করে আছে।
1// স্বাভাবিক অবস্থা2on SIGTERM:3 state.shuttingDown = true;4 // /readyz এখন 5035 wait(SHUTDOWN_DRAIN_MS);6 server.close();7 // চলমান request শেষ8 db.close(); exit 0;
| সময় | |
| /readyz | |
| /healthz | |
| নতুন সংযোগ | |
| /slow |
- স্বাভাবিক চলা।
/readyzদিচ্ছে 200,{"status":"ready","database":"ok"}. এরপরই ৮০০ ms-এর/slowকাজটি শুরু হবে। - SIGTERM এলো। Platform থামতে বলল।
/slowতখনও চলছে। এখনই process শেষ করলে ওই request হারাবে। - আগে readiness.
shuttingDowntrue, তাই/readyzদিল 503draining, কিন্তু/healthzএখনো 200. Process অসুস্থ নয়, শুধু বিদায় নিচ্ছে। Server এখনো request নেয়, কারণ load balancer খবর পেতে সময় নেয়। - Drain শেষ, close. ৩০০ ms পরে
server.close()ডাকা হলো। Listening socket বন্ধ, তবে চলমান connection কাটা হয়নি। - নতুন request ফেরত। এখন
/orders/42-এর নতুন সংযোগ পেলECONNREFUSED. বাস্তবে এমন হওয়ার কথা নয়, কারণ drain-এর মধ্যেই load balancer সরে যায়। ল্যাবে আমরা ইচ্ছা করে দেখলাম। - চলমান কাজ শেষ।
/slowনিরাপদে 200{"report":"done"}পেল, একইrequestId-সহrequest completedlog হলো। শুরু থেকে লেগেছে 816 ms. - পরিষ্কার বিদায়। শেষ সংযোগ বন্ধ হতেই
close-এর callback চলল, database বন্ধ হলো, log-এshutdown: clean exit, exit code 0.
আসল run-এ পাওয়া ফাঁদ। প্রথম সংস্করণে sweep ছিল না। তখন /slow শেষ হওয়ার পরও process প্রায় ৩.১ সেকেন্ড ঝুলে ছিল। কারণ, server.close() ডাকার সময় ফাঁকা connection বন্ধ করে, কিন্তু চলমান connection পরে ফাঁকা হলে সে keep-alive অবস্থায় বসে থাকে। ফলে close-এর callback আসে না। এমনকি ওই connection দিয়ে আরেকটি /readyz-ও উত্তর পেয়েছিল। closeIdleConnections() নিয়মিত ডাকার পর ফাঁকটি নেমে এলো প্রায় ৩৫ ms-এ (একটি লোকাল run). Keep-alive-এর কথা পর্ব ২-এ পেয়েছিলেন।
এবার সত্যিকারের signal, পাতাতেই। নিচের process নিজেই নিজেকে SIGTERM পাঠাচ্ছে।
const http = require("node:http");
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
let draining = false;
const server = http.createServer(async (req, res) => {
if (req.url === "/readyz") {
res.statusCode = draining ? 503 : 200;
return res.end(draining ? "draining" : "ready");
}
await wait(200); // একটি ধীর কাজ, যেমন order লেখা
res.end("order saved");
});
process.on("SIGTERM", () => {
console.log("SIGTERM received, readiness goes 503");
draining = true;
setTimeout(() => {
console.log("no new connections now");
const sweep = setInterval(() => server.closeIdleConnections(), 20);
server.close(() => { clearInterval(sweep); console.log("clean exit"); });
}, 60);
});
server.listen(0, "127.0.0.1", async () => {
const url = `http://127.0.0.1:${server.address().port}`;
const ask = (path) => fetch(url + path).then((r) => r.text())
.then((text) => console.log(`${path}: ${text}`))
.catch((err) => console.log(`${path}: ${err.cause.code}`));
await ask("/readyz");
ask("/order");
await wait(30);
process.kill(process.pid, "SIGTERM");
await wait(10);
await ask("/readyz");
await wait(100);
await ask("/readyz");
});আউটপুটের ক্রমই পুরো গল্প:
/readyz: ready
SIGTERM received, readiness goes 503
/readyz: draining
no new connections now
/readyz: ECONNREFUSED
/order: order saved
clean exit
Signal আসার পরও /order শেষ হয়েছে: order saved. কিন্তু close-এর পরে নতুন /readyz ঢুকতে পারেনি। Process নিজে থেকেই থেমেছে, কারণ আর কোনো কাজ বাকি নেই।
পুরো Express অ্যাপেও আসল kill -TERM চালানো হয়েছে (Linux, Node 24.15.0, drain ১০০০ ms, UUID ছোট করা):
$ kill -TERM <server pid>
$ curl readyz during drain
{"status":"draining"} 503
$ slow response:
{"report":"done"}
server exit code: 0
--- server log
{"level":"info","time":"2026-11-01T16:37:34.028Z","signal":"SIGTERM","msg":"shutdown: readiness now 503"}
{"level":"info","time":"2026-11-01T16:37:34.630Z","requestId":"c25a1613…","msg":"slow report finished"}
{"level":"info","time":"2026-11-01T16:37:34.631Z","req":{"method":"GET","url":"/slow"},"requestId":"c25a1613…","res":{"statusCode":200},"responseTime":801,"msg":"request completed"}
{"level":"info","time":"2026-11-01T16:37:35.028Z","msg":"shutdown: closing server"}
{"level":"info","time":"2026-11-01T16:37:35.028Z","msg":"shutdown: clean exit"}
Windows-এ সাবধান। Node-এর docs অনুযায়ী Windows-এ signal নেই; process.kill() দিয়ে SIGTERM পাঠালে process শর্তহীনভাবে শেষ হয়, handler চলে না। উপরের ECONNRESET run-টি ঠিক এটাই দেখায়। টার্মিনালের Ctrl+C-র SIGINT কাজ করে, তাই দুটি signal-ই ধরা হয়েছে। লোকালে Windows-এ পরীক্ষা করলে Ctrl+C ব্যবহার করুন।
হিসাব: কতক্ষণ অপেক্ষা করবেন
ধরে নেওয়া সংখ্যা: load balancer ৫ সেকেন্ড পরপর দেখে, ২ বার ব্যর্থ হলে সরায়। সবচেয়ে ধীর request ৮ সেকেন্ড। পরিষ্কারের কাজ ২ সেকেন্ড। Platform SIGKILL-এর আগে সময় দেয় ৩০ সেকেন্ড; Kubernetes-এর default-ও এটি, তবে আপনার পরিবেশ মিলিয়ে নিন।
মোট দরকার = 10 + 8 + 2 = 20 s < 30 s ✓
ঘড়ির হিসাবে ১০ সেকেন্ড অপেক্ষার পরই close. আর জোর করে থামানোর timer (আমাদের কোডের force) ২০ সেকেন্ডের বেশি, কিন্তু ৩০-এর কম হতে হবে। উদাহরণের ল্যাব কোডে এই মান কম রাখা হয়েছে, কারণ সেখানে কাজ ছোট।
৯ · Metrics — এক নজরে অ্যাপের নাড়ি
Log জবাব দেয়, “এই request-এ কী ঘটল?” Metrics জবাব দেয়, “এখন সব মিলিয়ে কেমন চলছে?” হাসপাতালের রোগীর ফাইল আর মনিটরের নাড়ির সংখ্যার মধ্যে যে তফাত। ন্যূনতম তিনটি সংখ্যা দিয়ে শুরু করুন: request-এর হার, error-এর হার, আর সময় (latency). prom-client package Prometheus-এর পড়ার মতো আকারে এগুলো দেয়।
import { Registry, Counter, Histogram } from 'prom-client';
export const registry = new Registry();
export const httpRequests = new Counter({
name: 'http_requests_total', help: 'HTTP requests',
labelNames: ['method', 'route', 'status'], registers: [registry],
});
export const httpDuration = new Histogram({
name: 'http_request_duration_seconds', help: 'HTTP latency',
labelNames: ['route'], buckets: [0.05, 0.1, 0.5, 1], registers: [registry],
});একটি ছোট middleware প্রতিটি response শেষে এই সংখ্যাগুলো বাড়ায়। আর GET /metrics সেগুলো লেখা আকারে দেয়। সত্যিকারের run-এর অংশ (prom-client 15.1.3):
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/orders/:id",status="200"} 5
http_requests_total{method="GET",route="/orders/:id",status="404"} 1
http_requests_total{method="POST",route="/login",status="200"} 1
…
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05",route="/orders/:id"} 6
http_request_duration_seconds_bucket{le="+Inf",route="/orders/:id"} 6
http_request_duration_seconds_sum{route="/orders/:id"} 0.1680047
http_request_duration_seconds_count{route="/orders/:id"} 6
ছোট হিসাব: /orders/:id-এর গড় সময় = 0.1680047 ÷ 6 ≈ 0.028 s, অর্থাৎ ২৮ ms. Histogram ৬টির সবকটিকেই ০.০৫ সেকেন্ডের ঘরে রেখেছে। আর error হার: ৬টির ১টি 404, তবে 404 সবসময় server-এর দোষ নয়, তাই error ধরুন 5xx-কে।
Label-এ route="/orders/:id" লিখুন, /orders/42 নয়। আর userId কখনো label নয়। প্রতিটি আলাদা মান একটি আলাদা সংখ্যার ধারা তৈরি করে। একে বলে label cardinality; এটি বেড়ে গেলে monitoring সিস্টেমের ওপর চাপ বাড়ে। /metrics-ও সবার জন্য খোলা রাখবেন না; এটি ভেতরের নেটওয়ার্ক বা সুরক্ষিত পথে রাখুন।
এখানে শুধু অ্যাপের ভেতরের অংশ দেখলাম। Error tracking, uptime আর alert পর্ব ৮৩-এ আসবে। আর অপারেশনের গভীর দিক আছে Cloud & DevOps কোর্সে: Metrics ও Prometheus, Centralized logging, Tracing আর Alerting ও SLO.
আরও গভীরে: একটি server পেরিয়ে একাধিক service-এ একই id নিয়ে যাওয়াকে distributed tracing বলে। Google-এর ২০১০ সালের Dapper গবেষণাপত্র এই চিন্তাকে পরিচিত করে, আর W3C Trace Context মান traceparent header ঠিক করে দিয়েছে। OpenTelemetry এখন এর বহুল ব্যবহৃত পথ, আর এর Node instrumentation-ও ভেতরে context ছড়াতে AsyncLocalStorage-ভিত্তিক ব্যবস্থা ব্যবহার করে। তাই আমাদের x-request-id এই বড় ধারণারই একটি সরল রূপ।
পরীক্ষা ও ইন্টারভিউতে:
- Liveness আর readiness-এর পার্থক্য? Liveness ব্যর্থ হলে process restart, readiness ব্যর্থ হলে শুধু traffic সরে।
- Request id প্রতিটি ফাংশনে না পাঠিয়ে কীভাবে পৌঁছাবেন? AsyncLocalStorage-এর
run()আরgetStore()দিয়ে। - Config কেন শুরুতে যাচাই করবেন? ভুল হলে deploy-এর মুহূর্তেই ধরা পড়ে, প্রথম ব্যবহারকারীর হাতে নয়।
- Graceful shutdown-এর ক্রম? SIGTERM, readiness 503, drain অপেক্ষা,
server.close(), সংযোগ বন্ধ, exit. - Log আর metrics-এর পার্থক্য? Log একক ঘটনার বিবরণ, metrics সময়ের সাথে সমষ্টিগত সংখ্যা।
১০ · AI-এর যুগে log আর config
“Express-এ logging যোগ করে দাও” বললে AI দ্রুত কোড দেবে, আর সেটি চলবেও। সমস্যা হলো, এই বিষয়ের ভুল চলার সময় চোখে পড়ে না। Password log-এ গেলে কোনো error আসে না। পড়ে একদিন, অনেক পরে।
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
log.info(req.body) বা পুরো req log করা | নকল password পাঠিয়ে log-এ খুঁজুন |
| pino-http-এর default serializer রেখে দেওয়া | Authorization header পাঠিয়ে log দেখুন |
পুরোনো pino API যেমন সরানো option বা prettyPrint | আপনার version-এর docs মিলিয়ে নিন |
process.env.SECRET || 'default-secret' | বাড়তি মান সরিয়ে দেখুন: server থামে কি না |
| Liveness-এ database query | Database বন্ধ করে দেখুন /healthz কী বলে |
SIGTERM-এ সরাসরি process.exit(0) | ধীর request চলাকালীন signal দিন |
| Error ধরে চুপ করে থাকা | কোডের প্রতিটি catch পড়ুন; log বা পুনরায় throw আছে কি না |
অপ্রয়োজনীয় package, যেমন Node 24-এ শুধু .env পড়তে dotenv | আগে দেখুন --env-file যথেষ্ট কি না |
AI-কে অস্পষ্ট কাজ না দিয়ে শর্ত বলে দিন, আর পরীক্ষার ধাপও চেয়ে নিন। আসল secret বা production log কখনো prompt-এ দেবেন না।
AI-এর লেখা কোড মেলানোর চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১JSON log মানুষের চোখে পড়া কঠিন। তাহলে সবাই JSON-এর দিকে গেল কেন? সাধারণ লেখা log কি কখনো ভালো?
Log-এর পাঠক বদলে গেছে, এটিই মূল কারণ। এক সময় একজন মানুষ একটি server-এ tail চালিয়ে পড়তেন। Apache-এর পুরোনো log আকার বা syslog সেই জগতের জন্য ঠিক ছিল। সমস্যা শুরু হয় যখন কয়েকশো server-এর log এক জায়গায় আসতে থাকে। তখন প্রথম পাঠক আর মানুষ নয়, একটি খোঁজার সিস্টেম। সে লেখা লাইনকে regex দিয়ে ভাঙে। কেউ বাক্যের শব্দ একটু বদলালেই regex ভাঙে, আর dashboard চুপচাপ শূন্য দেখায়।
Structured log সেই ভঙ্গুর স্তরটি সরিয়ে দেয়। orderId একটি field, তাই বার্তার ভাষা বদলালেও খোঁজা ঠিক থাকে। “গত এক ঘণ্টায় status=500, route অনুযায়ী গোনো” এমন প্রশ্নের উত্তর এক লাইনে পাওয়া যায়।
দাম আছে। JSON-এ প্রতিটি লাইনে key-র নাম বারবার লেখা হয়, তাই আকার বাড়ে, যদিও compression অনেকটা ফিরিয়ে আনে। আর চোখে পড়া কঠিন। এই কারণেই pino-pretty-র মতো টুল: যন্ত্রের জন্য JSON লেখা হয়, আর মানুষের জন্য পড়ার সময় রূপান্তর হয়। লেখার আকার আর দেখার আকার আলাদা করাই বুদ্ধির কাজ। JSON ছাড়া logfmt-এর মতো key=value আকারও প্রচলিত, যা মানুষ আর যন্ত্র দুজনের কাছেই সহজ।
সাধারণ লেখা কি কখনো ভালো? একবার চালানো ছোট script বা CLI টুলে অবশ্যই। সেখানে পাঠক একজন মানুষ, আর আউটপুট টিকে থাকে না। সেখানে JSON বাড়তি জটিলতা।
মূল উপলব্ধি: আগে জিজ্ঞেস করুন, এই log-এর প্রথম পাঠক কে? যন্ত্র হলে গঠন দিন, মানুষের জন্য দেখার সময় সুন্দর করুন।
প্র ০২Log তো শুধু লেখা। তাহলে logging কীভাবে নিজেই একটি নিরাপত্তা ঝুঁকি হয়ে ওঠে?
ঝুঁকি দুই দিক থেকে আসে: যা আপনি লিখছেন, আর যা ব্যবহারকারী আপনাকে দিয়ে লেখাচ্ছেন।
প্রথম দিক: ফাঁস। ২০১৮ সালে Twitter জানিয়েছিল, একটি bug-এর কারণে password hash হওয়ার আগেই একটি অভ্যন্তরীণ log-এ লেখা হচ্ছিল, এবং তারা ব্যবহারকারীদের password বদলাতে বলে। প্রকাশিত বক্তব্য অনুযায়ী কোনো অপব্যবহারের প্রমাণ মেলেনি, তবু এত মানুষকে সতর্ক করতে হয়েছিল। ঘটনাটি দেখায়, শক্ত hashing থাকলেও একটি log লাইন পুরো নিরাপত্তা পাশ কাটিয়ে যেতে পারে। Database-এর চেয়ে log-এর প্রবেশাধিকার সাধারণত ঢিলা, আর সেগুলো অন্য টুলেও কপি হয়।
দ্বিতীয় দিক: ইনপুট। ২০২১ সালের ডিসেম্বরে Java-র Log4j library-তে CVE-2021-44228, “Log4Shell” প্রকাশ পায়। Log করা লেখার ভেতরের একটি বিশেষ pattern library নিজেই ব্যাখ্যা করত, আর তার মাধ্যমে দূর থেকে কোড চালানো সম্ভব হতো। একটি User-Agent header log করাই যথেষ্ট ছিল। কোটি কোটি যন্ত্রের সফটওয়্যারে তাড়াহুড়ো করে patch দিতে হয়। pino এমন ব্যাখ্যা করে না, তবে শিক্ষা সাধারণ: log-এ যাওয়া ব্যবহারকারীর লেখাও অবিশ্বস্ত ইনপুট।
সাধারণ লেখা log-এ newline ঢুকিয়ে ভুয়া লাইন বানানোও সম্ভব, যাকে log injection বলে। JSON-এ newline escape হয় বলে এই ঝুঁকি কমে। এ কারণেই আমরা x-request-id যাচাই করেছি। আর log পড়ার dashboard যদি HTML ঠিকমতো escape না করে, সেখানে stored XSS-ও হতে পারে।
আইন আর নীতির দিকও আছে: ফোন নম্বর log-এ রাখলে, মুছে ফেলার অনুরোধ এলে সেই log-ও ভাবতে হয়। আপনার দেশ আর ব্যবসার নিয়ম আইনজ্ঞের সাথে মিলিয়ে নিন।
মূল উপলব্ধি: Log একটি ডেটাবেস, যা আপনি ডেটাবেস ভেবে সুরক্ষা দেননি। তাই ঢোকার সময় বাছুন, পড়ার অধিকার সীমিত রাখুন, আর নির্দিষ্ট সময় পরে মুছুন।
প্র ০৩Twelve-Factor বলে config থাকবে environment variable-এ। তাহলে কি environment variable secret রাখার সবচেয়ে নিরাপদ জায়গা?
না, আর এই দুটি কথাকে আলাদা করা দরকার। Adam Wiggins-এর Twelve-Factor App (প্রকাশ ২০১১ সালের দিকে, Heroku-র অভিজ্ঞতা থেকে) যে সমস্যা ধরেছিল তা হলো কোডে hard-coded config. সেই তুলনায় environment variable বিরাট উন্নতি: ভাষা-নিরপেক্ষ, deploy ভেদে সহজে বদলায়, ভুলে repository-তে যাওয়ার সম্ভাবনা কম।
কিন্তু “কোডের চেয়ে ভালো” মানে “সেরা” নয়। Environment variable-এর কিছু দুর্বলতা আছে:
- উত্তরাধিকার: child process সাধারণত পুরো environment পায়। একটি বাইরের টুল চালালে secret-ও তার হাতে যায়।
- অসাবধান প্রকাশ: debug route যদি সব env দেখায়, বা crash report পুরো environment পাঠায়, ফাঁস হয়ে যায়।
- আবর্তন কঠিন: মান process চালুর সময় বসে যায়। Secret বদলাতে সাধারণত restart লাগে।
- হিসাব নেই: কে কখন পড়ল, env থেকে তা জানা যায় না।
তাই বড় সিস্টেমে secret manager ব্যবহৃত হয়: কেন্দ্রীয় সুরক্ষিত ভাণ্ডার, প্রবেশের হিসাব, স্বয়ংক্রিয় আবর্তন, স্বল্পমেয়াদি credential. অনেকে সেখান থেকে চালুর সময় মান এনে env-এ বা মেমরিতে বসান, তাই দুটি পথ পরস্পরের শত্রু নয়। কিছু পরিবেশে secret ফাইল হিসেবে mount করা হয়, যা child process-এর হাতে আপনাআপনি যায় না।
ছোট প্রকল্পে কী করবেন? হোস্টিং প্ল্যাটফর্মের env সেটিংই সাধারণত যথেষ্ট, যদি কয়েকটি অভ্যাস মানেন: repository-তে secret নয়, প্রতিটি পরিবেশে আলাদা মান, প্রয়োজনের চেয়ে কম অধিকার, আর ফাঁস হলে বদলানোর পরিকল্পনা।
মূল উপলব্ধি: নিরাপত্তা কোনো একটি কৌশলের ঘোষণা নয়, বরং ঝুঁকি আর খরচের হিসাব। Env হলো ভালো ন্যূনতম মান; ঝুঁকি বাড়লে তার ওপরে স্তর যোগ করুন।
প্র ০৪Readiness-এ database দেখার পরামর্শ দেওয়া হলো। Database পড়ে গেলে সব instance একসাথে 503 দেবে। তাহলে এটিও কি বিপদ নয়?
প্রশ্নটি খুবই ন্যায্য, আর community এখানে পুরোপুরি একমত নয়। ধরুন, তিনটি instance আর একটি database. Database ত্রিশ সেকেন্ডের জন্য অদৃশ্য হলো। তিনটিই readiness ব্যর্থ করল। Load balancer-এর কাছে কোনো সুস্থ server নেই। ক্রেতারা পেলেন load balancer-এর নিজের 502 বা 503 পাতা।
অন্যদিকে readiness-এ database না দেখলে request অ্যাপে পৌঁছাবে, আর অ্যাপ একটি সাজানো error দেবে। Cache থেকে পণ্যের তালিকা তখনও দেখানো যেতে পারে। অর্থাৎ কিছু কাজ database ছাড়াও চলতে পারে, এবং সব কাজ বন্ধ করে দিলে সেই সুযোগ হারায়।
দুই পক্ষের যুক্তি মিলিয়ে একটি মাঝামাঝি পথ:
- Database যদি অ্যাপের প্রায় সব কাজে লাগে, যেমন আমাদের order API, তাহলে readiness-এ check রাখা ঠিক।
- কিছু instance নির্দিষ্ট database replica-তে নির্ভর করলে check বিশেষ কাজে লাগে, কারণ শুধু সেই instance থেকে traffic সরে।
- একটি ঐচ্ছিক dependency, যেমন email সেবা, readiness-এ রাখবেন না। তার জন্য পুরো অ্যাপ বন্ধ হওয়া উচিত নয়।
- একবার ব্যর্থতায় সরাবেন না। কয়েকবার ব্যর্থ হলে সরান, আর ফলটি অল্প সময়ের জন্য cache করুন, যাতে check নিজেই database-কে চাপ না দেয়।
Kubernetes-এর docs liveness নিয়ে বিশেষভাবে সতর্ক করে। Readiness ব্যর্থ হলে অন্তত restart ঝড় হয় না, আর database ফিরলে অ্যাপ নিজেই আবার যোগ দেয়। এই কারণেই readiness-এর ভুল তুলনায় কম ক্ষতিকর। Google-এর SRE বইয়েও এমন আংশিক ব্যর্থতায় সেবা ছোট করে চালানোর (graceful degradation) ধারণা গুরুত্ব পেয়েছে।
মূল উপলব্ধি: Health check কোনো খালি রুটিন নয়, এটি ব্যর্থতার সময় সিস্টেমের আচরণ ঠিক করে। তাই লেখার আগে ভাবুন: এই check ব্যর্থ হলে কে কী করবে, আর সেটি সমস্যা কমাবে না বাড়াবে?
প্র ০৫Graceful shutdown এত যত্নে লিখলাম। কিন্তু বিদ্যুৎ চলে গেলে তো কিছুই চলবে না। তাহলে এর ওপর কতটা ভরসা করা উচিত?
ভরসা করা উচিত, কিন্তু একমাত্র ভরসা হিসেবে নয়। লোডশেডিং, সার্ভারের হার্ডওয়্যার নষ্ট, মেমরি ফুরিয়ে OS-এর হাতে process মারা পড়া, SIGKILL, কোনো ক্ষেত্রেই আপনার handler চলবে না। Graceful shutdown ভালো দিনের ব্যবস্থা: নিয়মিত deploy-এ ব্যবহারকারী যেন কিছু টের না পান। খারাপ দিনের ব্যবস্থা আলাদা।
২০০৩ সালে George Candea আর Armando Fox “Crash-Only Software” গবেষণাপত্রে একটি উল্টো প্রস্তাব দেন। যে সফটওয়্যার যেকোনো মুহূর্তে পড়ে গিয়েও নিরাপদে ফিরতে পারে, তার আলাদা “পরিষ্কার বন্ধ” পথের দরকার কম। কারণ দুটি পথ রাখলে কম ব্যবহৃত পথটিতেই bug থাকে। Crash-ই একমাত্র বন্ধের পথ হলে সেটি সবসময় পরীক্ষিত হয়।
বাস্তবে দুটি চিন্তা একসাথে চলে। Web backend-এ এর মানে:
- Transaction: order আর stock একসাথে লেখা হবে বা কিছুই না (পর্ব ৪৩). মাঝপথে process মরলে database ফিরিয়ে নেবে।
- Idempotency: ক্লায়েন্ট একই order আবার পাঠালে দুবার টাকা কাটবে না (পর্ব ৩৩ ও ৬৫).
- Background job: কাজ queue-তে থাকে, শেষ হওয়ার স্বীকৃতি না পেলে আবার চলে (পর্ব ৬২).
- Timeout:
forcetimer ছাড়া একটি আটকে যাওয়া connection চিরকাল shutdown ঠেকাতে পারে।
আমাদের ল্যাবের ECONNRESET-এর কথা ভাবুন। ক্লায়েন্ট জানে না order হয়েছে কি না। Graceful shutdown এই ঘটনা কমায়, কিন্তু Idempotency ঘটনাটি ঘটলেও ক্ষতি ঠেকায়। প্রথমটি সংখ্যা কমায়, দ্বিতীয়টি ফল নিরাপদ করে।
মূল উপলব্ধি: ধরে নিন process যেকোনো মুহূর্তে মরতে পারে। ডেটা সেভাবেই নিরাপদ রাখুন। Graceful shutdown তার ওপরে ব্যবহারকারীর অভিজ্ঞতা মসৃণ করার একটি স্তর, ভিত্তি নয়।
অনুশীলন
হিসাব, কমান্ড, সমালোচনা আর আউটপুট অনুমান, চারটিই আছে।
-
Shutdown বাজেট (হিসাব): Load balancer ১০ সেকেন্ড পরপর দেখে, ৩ বার ব্যর্থ হলে সরায়। সবচেয়ে ধীর request ১৫ সেকেন্ড, পরিষ্কারের কাজ ৩ সেকেন্ড, platform SIGKILL দেয় ৩০ সেকেন্ডে। ন্যূনতম drain আর মোট সময় কত? সব কি ৩০ সেকেন্ডে আঁটবে? না হলে কী করবেন?
Drain ≥ 10 × 3 = ৩০ সেকেন্ড। মোট = 30 + 15 + 3 = ৪৮ সেকেন্ড, যা ৩০-এর চেয়ে ১৮ সেকেন্ড বেশি। তাই SIGKILL চলমান request কেটে দেবে।
পথগুলো: platform-এর সময় অন্তত ৫০ সেকেন্ড করা; check-এর সময় ৫ সেকেন্ড ও threshold ২ করে drain ১০ সেকেন্ডে আনা, তখন মোট 10 + 15 + 3 = ২৮ সেকেন্ড; অথবা ধীর কাজ background job-এ সরানো। তবে check খুব ঘন করলে সামান্য ঝাঁকুনিতেই server সরে যেতে পারে।
-
নিজের হাতে যাচাই (কমান্ড): একটি Express অ্যাপে এই পর্বের config আর health check বসান। তারপর এই তিনটি পরীক্ষা চালান: (ক)
SESSION_SECRETছাড়া চালানো (খ)curl -iদিয়ে/readyzডাকা (গ) একটি request-এ-H "x-request-id: test-order-0001"দিয়ে log-এ সেই id খোঁজা। কী দেখার কথা?(ক)
Invalid config,SESSION_SECRET-এর নামসহ বার্তা, exit code 1, port বন্ধ। (খ)HTTP/1.1 200 OKআর{"status":"ready","database":"ok"}. (গ) Response-এx-request-id: test-order-0001ফেরে, আর সেই request-এর প্রতিটি log লাইনে একই id থাকে, কারণ এটিSAFE_IDregex-এ মেলে। -
কোড সমালোচনা (নকশা): একজন সহকর্মী এই কোড এনেছেন। অন্তত পাঁচটি সমস্যা ধরুন:
const secret = process.env.SECRET || 'dev'; app.use((req,res,next)=>{ console.log(req.headers, req.body); next(); }); app.get('/health', async (req,res)=>{ await db.query('SELECT count(*) FROM orders'); res.send('ok'); }); process.on('SIGTERM', ()=>process.exit(0));|| 'dev': production-এ মান না থাকলে একটি জানা secret নিয়ে চলবে। Zod দিয়ে যাচাই করে থেমে যান।console.log: গঠনহীন, level নেই, request id নেই।req.headers, req.body: token, cookie, password সরাসরি log-এ।- একটি
/healthদিয়ে দুই কাজ: liveness আর readiness আলাদা নয়। Liveness হিসেবে ব্যবহার হলে database-এর সমস্যায় restart ঝড় হবে। count(*): পুরো table গোনা ভারী কাজ, প্রতি কয়েক সেকেন্ডে চলবে।SELECT 1যথেষ্ট, আর timeout দরকার। ব্যর্থ হলে সাজানো 503-ও নেই।- SIGTERM-এ
process.exit(0): চলমান request কেটে যাবে; readiness 503, drain, close কিছুই নেই।
-
আউটপুট অনুমান:
.env-এPORT=3000আরLOG_LEVEL=info,.env.test-এLOG_LEVEL=warnআরPORT=3100. Shell-এPORTআগে থেকেই4000. কমান্ড:node --env-file=.env --env-file=.env.test app.js.app.jsশুধুPORTআরLOG_LEVELছাপে। কী ছাপবে?PORT = 4000,LOG_LEVEL = warn. Environment-এ আগে থাকা মান সব ফাইলের ওপরে জেতে, তাইPORT4000;3100কাজে আসে না।LOG_LEVELenvironment-এ ছিল না, তাই ফাইল থেকে আসে, আর পরের ফাইল আগেরটিকে ছাপিয়েwarnকরে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমGraceful shutdown-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Process SIGTERM পেল
/readyz503 দেওয়া শুরু করল- Load balancer সরে যাওয়ার জন্য drain অপেক্ষা
server.close(): নতুন সংযোগ বন্ধ- চলমান request শেষ, ফাঁকা সংযোগ বন্ধ
- Database বন্ধ করে exit code 0
একটি logger-এর ন্যূনতম level info. debug, info, warn, error, চারটি level-এ একটি করে লাইন লেখার চেষ্টা হলো। কতটি লাইন লেখা হবে?
- ১টি, শুধু
info - ৩টি:
info,warn,error - ৪টিই
- ২টি:
warn,error
Node.js-এ AsyncLocalStorage request id-র কোন সমস্যার সমাধান করে?
- অনন্য id তৈরি করে
- Log ফাইল ডিস্কে লেখে
- একসাথে চলা request-এর প্রতিটির async কাজে আলাদা মান পৌঁছে দেয়, argument ছাড়াই
- একাধিক server-এর মধ্যে মান ভাগ করে নেয়
pino-তে redact: ['*.password'] দেওয়া আছে। { body: { password: 'a', profile: { password: 'b' } } } log করলে কী হবে?
- দুটি password-ই ঢাকা পড়বে
- শুধু
body.passwordঢাকা পড়বে,body.profile.passwordখোলা থাকবে - কোনোটিই ঢাকা পড়বে না
- পুরো লাইনটি লেখা হবে না
Shell-এ PORT=4000 আছে, আর .env-এ PORT=3000. node --env-file=.env app.js চালালে process.env.PORT কত হবে?
- 3000
- 40003000
- Error দিয়ে থেমে যাবে
- 4000
একটি API-র database কিছুক্ষণের জন্য বন্ধ। Process ঠিকমতো চলছে। ভালো নকশায় কী হওয়া উচিত?
/healthz200,/readyz503/healthz503,/readyz503/healthz503,/readyz200/healthz200,/readyz200
একটি HTTP metric-এর label হিসেবে কোনটি সবচেয়ে উপযুক্ত?
userId="8421"path="/orders/42"route="/orders/:id"requestId="8e08e93d"
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Production log হবে JSON, level-সহ, stdout-এ; pino আর pino-http এই কাজ করে।
- AsyncLocalStorage আর pino
mixinমিলে প্রতিটি লাইনেrequestIdআনে; বাইরের id যাচাই করে নিন। - পুরো
req.bodyবা header log নয়; বেছে নেওয়া field লিখুন,redactশেষ বেড়া, আর*মাত্র এক স্তর। - Config environment-এ, secret repository-র বাইরে; শুরুতেই Zod দিয়ে যাচাই করে থেমে যান।
/healthzশুধু process,/readyzdatabase আর shutdown দেখে; থামার ক্রম 503 → drain → close → exit.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩২ · REST resource নকশা — URL, method আর সম্পর্কপরবর্তী পর্বAPI নকশার মডিউলের শুরু।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ।
- Cybersecurity · Security loggingগভীরে যাননিরাপত্তার জন্য কী রাখবেন, কী রাখবেন না।
- Cloud & DevOps · Centralized loggingপাশের ট্র্যাকঅনেক server-এর log এক জায়গায় আনা।
- Cloud & DevOps · Deployment strategiesপাশের ট্র্যাকReadiness আর drain কীভাবে নিরাপদ deploy-এ কাজে লাগে।