এই পর্বে যা শিখবেন
- একটি ফুল-স্ট্যাক অ্যাপে কী কী দেখবেন: চারটি golden signal আর ব্যবসার সংকেত
- Sentry-র JavaScript SDK কী পাঠায়, নিজের লেখা receiver-এ আসল envelope পড়ে দেখা
- Next.js 16-এ
onRequestError,error.tsx,global-error.tsxআর browser-এর ভুল ধরা, আসল run-সহ - Source map, error grouping আর পাঠানোর আগে ব্যক্তিগত তথ্য মোছা
- prom-client-এর
/metrics, OpenTelemetry trace, uptime script, SLO, burn rate alert আর ছোট দলের on-call
১ · কী দেখবেন — অ্যাপের নাড়ি কোথায়
“বাজারঘর” এখন production-এ। Build হয়েছে, deploy হয়েছে, health check সবুজ। তারপর এক রাতে একজন ক্রেতা ফেসবুকে লিখলেন, “চেকআউটে বোতাম চাপলে কিছুই হয় না।” Server-এর log পরিষ্কার, কারণ ভুলটি ঘটেছে ক্রেতার browser-এ, আর সেখান থেকে কোনো খবর আসে না। আপনি জানলেন তিন ঘণ্টা পরে, তাও একজন রাগী ক্রেতার কাছ থেকে।
পর্ব ৩১-এ (Log আর config) আপনি structured log, request id, health check আর প্রাথমিক metric বসিয়েছিলেন। সেগুলো server-এর ভেতরের খবর। এই পর্বে পুরো ছবি: browser থেকে database, ভুল থেকে alert, আর alert থেকে ঘুম ভাঙানো মানুষ পর্যন্ত। তাত্ত্বিক গভীরতা Cloud & DevOps কোর্সে আছে; এখানে একজন ফুল-স্ট্যাক ডেভেলপারের হাতের কাজ।
হাসপাতালের ICU-র কথা ভাবুন। রোগীর বিছানার পাশে মনিটরে নাড়ি, রক্তচাপ, অক্সিজেন চলছে (metric). নার্স খাতায় প্রতিটি ঘটনা লিখছেন (log). কোনো ওষুধে প্রতিক্রিয়া হলে আলাদা রিপোর্ট হয়, কখন, কোন ওষুধ, আগে কী খেয়েছিলেন (error report). আর একটি অ্যালার্ম শুধু তখনই বাজে, যখন রোগীর অবস্থা সত্যিই খারাপ হচ্ছে। প্রতিটি ছোট ওঠানামায় অ্যালার্ম বাজলে নার্সরা কিছুদিন পর আর শোনেন না।
চারটি golden signal আর ব্যবসার সংকেত
Google-এর Site Reliability Engineering বই (২০১৬) ব্যবহারকারীর দিকের সেবার জন্য চারটি মাপকে ন্যূনতম তালিকা বলে: latency, traffic, errors আর saturation. ফুল-স্ট্যাক অ্যাপে এর সাথে দুটি কথা যোগ করুন: ভুল শুধু server-এ নয়, browser-এও হয়; আর প্রযুক্তির সংখ্যা সবুজ থাকলেও ব্যবসা থেমে থাকতে পারে।
| সংকেত | প্রশ্ন | বাজারঘরে কী মাপবেন | কোথা থেকে |
|---|---|---|---|
| Latency | কত দ্রুত? | /api/checkout-এর p95, সফল আর ব্যর্থ আলাদা | histogram, trace |
| Traffic | কত চাপ? | প্রতি সেকেন্ডে request, route অনুযায়ী | counter |
| Errors | কতটা ভুল? | 5xx-এর হার, browser-এর JavaScript ভুল | counter, error tracker |
| Saturation | কতটা ভরা? | DB connection pool, event loop lag, memory | gauge |
| ব্যবসা | টাকা আসছে? | প্রতি ঘণ্টায় order, payment ব্যর্থতা, cart থেকে checkout-এর হার | নিজের counter |
| বাইরে থেকে | সাইট খুলছে? | uptime আর synthetic check | বাইরের checker |
শেষ দুটি সারি কেন জরুরি? ধরুন payment gateway-এর webhook (পর্ব ৬৫, Payment integration) আসা বন্ধ হয়ে গেছে। Server কোনো ভুল দেখাচ্ছে না, কারণ কোনো request-ই আসছে না। Latency চমৎকার, error শূন্য। শুধু “প্রতি ঘণ্টায় সফল order” সংখ্যাটি হঠাৎ শূন্যে নেমেছে। এই সংখ্যাই একমাত্র সাক্ষী।
বাজারঘরের monitoring-এর মানচিত্র
Errors, logs, metrics and traces leave the app on different paths
২ · Error tracking কীভাবে কাজ করে — আসল SDK, নিজের receiver
Log-এ একটি ভুলের লাইন থাকে। Error tracking টুল তার চেয়ে বেশি করে: প্রতিটি ভুল প্রসঙ্গসহ ধরে, একই ধরনের ভুল এক জায়গায় জড়ো করে, কতজন ব্যবহারকারী আক্রান্ত তা গোনে, আর নতুন ধরনের ভুল এলে খবর দেয়। Sentry এই ধারার পরিচিত একটি টুল; শুরুতে (প্রায় ২০০৮ সালে) এটি Django-র একটি ছোট error logging plugin হিসেবে জন্মায় বলে জানা যায়। একই ধরনের hosted সেবা আরও আছে, যেমন Bugsnag, Rollbar, Datadog বা New Relic-এর error অংশ; আর Sentry নিজে self-hosted-ও চালানো যায়। কোনোটি বাছার আগে নিজের প্রয়োজন, ডেটা কোথায় থাকবে আর খরচ তাদের বর্তমান docs থেকে মিলিয়ে নিন।
ডাকঘরের বাছাইঘরের মতো। প্রতিটি চিঠি (event) আসে খামে, খামের ওপর ঠিকানা আর ছাপ (header), ভেতরে চিঠি (payload). বাছাইকারী একই এলাকার চিঠি এক বস্তায় রাখেন (grouping). Error tracker ঠিক এটাই করে, শুধু চিঠির বদলে ভুল।
কাজটি কী, তা দেখার সবচেয়ে ভালো উপায় হলো SDK যা পাঠায় তা নিজে পড়া। তাই আসল Sentry account ছাড়াই একটি পরীক্ষা: Sentry-র official Node SDK (@sentry/node 11.10.0, লেখার সময়ের সর্বশেষ stable) চালানো হলো একটি নকল DSN দিয়ে, যা নির্দেশ করে 127.0.0.1-এ আমাদের লেখা ৪০ লাইনের একটি ছোট server-এর দিকে। সেই server শুধু যা আসে তা ছাপে।
import * as Sentry from '@sentry/node';
const PHONE = /\b01[3-9]\d{8}\b/g;
Sentry.init({
dsn: 'http://examplepublickey@127.0.0.1:47831/1', // নকল key, লোকাল receiver
release: 'bazarghar-api@1.4.2',
environment: 'production',
sendDefaultPii: false,
tracesSampleRate: 0,
beforeSend(event) {
// শেষ বেড়া: event-এর যেকোনো জায়গায় ফোন নম্বর ঢেকে দেওয়া
return JSON.parse(JSON.stringify(event).replace(PHONE, '[phone]'));
},
});
Sentry.setTag('route', '/api/checkout');
Sentry.setUser({ id: 'u_8421' });
Sentry.addBreadcrumb({ category: 'cart', message: 'added product 77, qty 2' });
Sentry.addBreadcrumb({ category: 'payment', message: 'gateway call started' });
function chargeCustomer(order) {
if (!order.gatewayToken) {
throw new TypeError(`gateway token missing for ${order.phone}`);
}
}
try {
chargeCustomer({ id: 1042, phone: '01712345678' });
} catch (err) {
Sentry.captureException(err, { extra: { orderId: 1042, phone: '01712345678' } });
}
await Sentry.flush(2000);সত্যিকারের run (Node 24.15.0, @sentry/node 11.10.0). Receiver তিনটি POST পেল। প্রথম আর শেষটি session-এর খবর; মাঝেরটি আসল ভুল। Receiver payload থেকে দরকারি ঘরগুলো বেছে ছেপেছে; পড়ার সুবিধায় JSON-এর কয়েকটি ঘর এক লাইনে সাজানো:
--- POST /api/1/envelope/?sentry_version=7&sentry_key=examplepublickey&sentry_client=sentry.javascript.node%2F11.10.0 (4438 bytes)
envelope header: {"event_id":"98af99fc3a424049aa116f2528e99050","sent_at":"2026-11-03T11:16:30.989Z","sdk":{"name":"sentry.javascript.node","version":"11.10.0"},"trace":{"environment":"production","release":"bazarghar-api@1.4.2","public_key":"examplepublickey","trace_id":"679de9bd4adb444bb78e6adc2f6afe0e"}}
item header: {"type":"event"}
{
"type": "TypeError",
"value": "gateway token missing for [phone]",
"release": "bazarghar-api@1.4.2",
"environment": "production",
"tags": { "route": "/api/checkout" },
"user": { "id": "u_8421" },
"level": "error",
"topFrame": { "function": "chargeCustomer", "file": "sentry-demo.mjs", "lineno": 24, "colno": 11, "in_app": true },
"breadcrumbs": [ "cart: added product 77, qty 2", "payment: gateway call started" ],
"extra": { "orderId": 1042, "phone": "[phone]" }
}
প্রতিটি অংশ কী বলে:
- URL আর DSN. DSN-এর
examplepublickey@127.0.0.1:47831/1ভেঙে SDK বানাল/api/1/envelope/: project 1, আর key query-তে। এই key গোপন নয়, browser-এর কোডেও থাকে; তাই যে কেউ আপনার project-এ event পাঠাতে পারে। Rate limit আর বাছাইয়ের দায়িত্ব server-এর। - Envelope. প্রথম লাইন খামের header, তারপর প্রতিটি item-এর জন্য একটি header লাইন আর একটি payload লাইন। একই খামে event, session, attachment একসাথে যেতে পারে।
- Release আর environment. কোন version-এর পরে ভুলটি শুরু হলো, production না staging, এই দুটি tag ছাড়া উত্তর পাওয়া কঠিন। Release-এর নাম build-এর সময়েই ঠিক হয় (পর্ব ৭৯, Build আর bundle), আর একই নাম source map upload-এও ব্যবহার হয়।
- Breadcrumb. ভুলের আগের ছোট ঘটনার সারি। Browser SDK সাধারণত ক্লিক, navigation আর
fetchনিজে থেকেই breadcrumb বানায়। - Stack frame.
in_app: trueমানে frame-টি আপনার কোডের, library-র নয়। Grouping-এ এদের গুরুত্ব বেশি। - Scrubbing. Message আর
extra, দুই জায়গাতেই ফোন নম্বর[phone]হয়েছে, কারণbeforeSendপুরো event পাঠানোর আগে বদলায়।
আসল run-এ ধরা পড়া ব্যাপার। Receiver-এ পুরো event-এর key-গুলো ছাপলে দেখা গেল: exception, event_id, level, platform, contexts, server_name, timestamp, environment, release, sdk, extra, tags, user, breadcrumbs, modules. আর contexts-এর ভেতরে trace, runtime, app, os, device, culture, cloud_resource. অর্থাৎ default-এ machine-এর নাম (server_name), OS আর সব installed package-এর তালিকা (modules) চলে যায়। এগুলো ব্যক্তিগত তথ্য নয়, কিন্তু আপনার পরিকাঠামোর খবর। দরকার না হলে SDK-র option বা beforeSend-এ বাদ দিন, আর আপনার version-এর docs মিলিয়ে নিন।
একই SDK-র browser সংস্করণ (@sentry/browser, Next.js-এর জন্য @sentry/nextjs) ঠিক এই খামেই পাঠায়। ফারাক শুধু ভুল ধরার উৎসে: browser-এ window-এর event, server-এ process-এর uncaughtException আর unhandled rejection. Express-এর central error handler (পর্ব ২৮, Error handling) যে ভুল নিজে সামলায়, সেগুলো SDK-কে দিয়ে আলাদা করে ধরাতে হয়; প্রতিটি টুলের Express integration-এর docs দেখুন।
৩ · Next.js 16-এ ভুল ধরা — server, render আর browser
Next.js অ্যাপে ভুল তিন জায়গায় জন্মায়, আর প্রতিটির দরজা আলাদা। নিচের সব কোড Next.js 16.4.4 আর React 19.3.1-এ next build আর next start দিয়ে সত্যিই চালানো, আর browser অংশ Edge-এ Playwright দিয়ে চাপা। API মিলিয়ে নেওয়া হয়েছে nextjs.org-এর instrumentation.js আর error.js পাতার সাথে।
দরজা ১: server — instrumentation.ts আর onRequestError
Project-এর মূলে (বা src/-এ) instrumentation.ts রাখলে Next.js server চালুর সময় একবার register() ডাকে। Instrumentation-এর SDK এখানেই বসে। আর onRequestError ডাকা হয় যখন server-এ কোনো ভুল ধরা পড়ে: Server Component render, route handler, server action বা proxy-তে। Docs-এর version তালিকা অনুযায়ী এটি এসেছে v15.0.0-তে।
import type { Instrumentation } from 'next';
import { scrubText } from './lib/scrub';
export function register() {
console.log(JSON.stringify({ msg: 'instrumentation registered', runtime: process.env.NEXT_RUNTIME }));
}
export const onRequestError: Instrumentation.onRequestError = async (err, request, context) => {
const message = err instanceof Error ? err.message : String(err);
const digest =
typeof err === 'object' && err !== null && 'digest' in err ? String(err.digest) : undefined;
const report = {
source: 'server',
message: scrubText(message),
digest,
method: request.method,
path: request.path.split('?')[0], // query-তে token থাকতে পারে
routePath: context.routePath,
routeType: context.routeType,
renderSource: context.renderSource,
release: process.env.RELEASE ?? 'dev',
};
console.log(JSON.stringify(report)); // আসল অ্যাপে এখানে await করে পাঠানো হবে
};একটি Server Component ইচ্ছা করে ভুল ছুড়ছে। বার্তায় একটি ফোন নম্বরও আছে, কারণ বাস্তবের কোডে এমন হয়:
export default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
if (id === '13') {
throw new Error(`stock lookup failed for order ${id}, customer 01712345678`);
}
return <h1>Order {id}</h1>;
}সত্যিকারের run. RELEASE=web@2.3.0 দিয়ে চালু করে browser-এ /orders/13 খোলা হলো। Server-এর আউটপুট (path ছোট করা):
▲ Next.js 16.4.4
✓ Ready in 142ms
{"msg":"instrumentation registered","runtime":"nodejs"}
⨯ Error: stock lookup failed for order 13, customer 01712345678
at c (~/bazarghar/.next/server/chunks/ssr/[root-of-the-server]__1unl47t2wpdw9._.js:1:1610) {
digest: '1336287772'
}
{"source":"server","message":"stock lookup failed for order 13, customer [phone]","digest":"1336287772","method":"GET","path":"/orders/13","routePath":"/orders/[id]","routeType":"render","renderSource":"react-server-components","release":"web@2.3.0"}
দুটি জিনিস খেয়াল করুন। এক, routePath হলো ছাঁচ /orders/[id], আসল path নয়; grouping আর metric-এর label-এর জন্য এটিই দরকার। দুই, আমাদের report-এ ফোন নম্বর ঢাকা, কিন্তু Next.js-এর নিজের ⨯ Error লাইনে নম্বরটি খোলা! অর্থাৎ পাঠানোর সময় মোছা যথেষ্ট নয়; ব্যক্তিগত তথ্য ভুলের বার্তায় না রাখাই প্রথম নিয়ম। Digest 1336287772 সংখ্যাটি মনে রাখুন।
দরজা ২: render — error.tsx আর global-error.tsx
error.tsx একটি route segment-কে React-এর error boundary-তে মোড়ে (পর্ব ২৩, React performance). এটি অবশ্যই Client Component. Next.js 16-এর docs অনুযায়ী prop দুটি error (ধরন unknown) আর retry, যা v16.3.0-তে stable হয়েছে; পুরোনো reset এখনো আছে, তবে docs বেশিরভাগ ক্ষেত্রে retry দেখায়। Root layout-এর ভুল ধরে global-error.tsx, যাকে নিজের <html> আর <body> দিতে হয়।
'use client';
import { useEffect } from 'react';
import { report } from '../../report';
export default function OrderError({ error, retry }: { error: unknown; retry: () => void }) {
useEffect(() => {
const e = error instanceof Error ? (error as Error & { digest?: string }) : null;
report({ source: 'error.tsx', message: e?.message ?? String(error), digest: e?.digest });
}, [error]);
return (
<div>
<h2>অর্ডারটি এখন দেখানো যাচ্ছে না</h2>
<button onClick={() => retry()}>আবার চেষ্টা করুন</button>
</div>
);
}একই run-এ browser থেকে আসা report, যা আমাদের নিজের route handler /api/client-errors পেল:
{"received":"client-error","source":"error.tsx","message":"Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.","digest":"1336287772","page":"/orders/13"}
Browser আসল বার্তা পায়নি, পেয়েছে React-এর একটি ছোট করা সাধারণ বার্তা। এটি নকশা করেই: production-এ Server Component-এর ভুলের বিবরণ client-এ পাঠানো হয় না, যাতে গোপন তথ্য ফাঁস না হয়। কিন্তু digest হুবহু এক, 1336287772. এই সংখ্যা দিয়েই browser-এর report আর server-এর report জোড়া লাগে। Error tracking টুলের Next.js SDK এই জোড়ার কাজটি নিজেই করে।
দরজা ৩: browser — error আর unhandledrejection
Event handler-এর ভেতরের ভুল error boundary ধরে না, কারণ সেটি render-এর সময় ঘটে না। Async কাজের ধরা না পড়া promise-ও না। এদের জন্য window-এর দুটি event. Root layout-এ একটি ছোট Client Component বসানো হলো:
'use client';
import { useEffect } from 'react';
import { report } from './report';
export default function ErrorReporter() {
useEffect(() => {
const onError = (e: ErrorEvent) =>
report({ source: 'window.onerror', message: e.message, file: e.filename, line: e.lineno, col: e.colno });
const onRejection = (e: PromiseRejectionEvent) =>
report({ source: 'unhandledrejection', message: String(e.reason?.message ?? e.reason) });
window.addEventListener('error', onError);
window.addEventListener('unhandledrejection', onRejection);
return () => {
window.removeEventListener('error', onError);
window.removeEventListener('unhandledrejection', onRejection);
};
}, []);
return null;
}export function report(payload: Record<string, unknown>) {
const body = JSON.stringify({ ...payload, page: location.pathname });
// sendBeacon পাতা বন্ধ হওয়ার মুহূর্তেও পাঠায়; না পারলে keepalive fetch
if (!navigator.sendBeacon?.('/api/client-errors', body)) {
fetch('/api/client-errors', { method: 'POST', body, keepalive: true });
}
}/checkout পাতায় দুটি বোতাম: একটি event handler-এ null-এর property পড়ে, অন্যটি একটি promise reject করে কেউ ধরে না। Playwright Edge-এ দুটিই চাপল। Server-এর log-এ এলো:
{"received":"client-error","source":"window.onerror","message":"Uncaught TypeError: Cannot read properties of null (reading 'total')","file":"http://127.0.0.1:47835/_next/static/chunks/0e0yfs38z2o_4.js","line":1,"col":281,"page":"/checkout"}
{"received":"client-error","source":"unhandledrejection","message":"payment status poll failed","page":"/checkout"}
প্রথম লাইনের সমস্যা দেখছেন? ভুলের জায়গা 0e0yfs38z2o_4.js-এর লাইন 1, column 281. Production build-এ পুরো ফাইল এক লাইনের। এখান থেকে আসল কোড খুঁজে পাওয়া পরের অংশের কাজ।
Receiving route handler-টিও যত্নে লেখা, কারণ এটি ইন্টারনেটের যে কারও জন্য খোলা (পর্ব ৫৭-এর route handler): আকারের সীমা, শুধু চেনা field, প্রতিটি string ছেঁটে ছোট করা।
import { scrubText } from '../../../lib/scrub';
const ALLOWED = ['source', 'message', 'digest', 'file', 'line', 'col', 'page'];
export async function POST(request: Request) {
const raw = await request.text();
if (raw.length > 8_000) return new Response(null, { status: 413 });
let input: Record<string, unknown>;
try { input = JSON.parse(raw); } catch { return new Response(null, { status: 400 }); }
const event: Record<string, unknown> = {};
for (const key of ALLOWED) {
const v = input[key];
if (typeof v === 'string') event[key] = scrubText(v).slice(0, 500);
else if (typeof v === 'number') event[key] = v;
}
console.log(JSON.stringify({ received: 'client-error', ...event }));
return new Response(null, { status: 204 });
}নিজে বানাবেন না hosted টুল? উপরের কোড শেখার জন্য আর খুব ছোট সাইটের জন্য যথেষ্ট। কিন্তু grouping, source map, ব্যবহারকারী গোনা, alert, ভিড়ের সময় rate limit, এসব নিজে লিখতে অনেক সময় লাগে। তাই বেশিরভাগ দল একটি টুলের SDK ব্যবহার করে, আর নিজে লেখে শুধু scrubbing আর কোন ভুল পাঠাবে তার নিয়ম। মাঝামাঝি পথও আছে: Sentry-র protocol বোঝে এমন open-source server নিজে চালানো (যেমন GlitchTip, তাদের docs অনুযায়ী)।
৪ · Source map আর grouping — minified লাইন থেকে একটি issue
Build-এর সময় minification নাম ছোট করে, ফাঁকা জায়গা মোছে, অনেক ফাইল এক করে (পর্ব ৭৯-এ বিস্তারিত)। ফলে stack trace আর মানুষের পড়ার মতো থাকে না। Source map একটি JSON ফাইল, যা minified ফাইলের প্রতিটি অবস্থানকে মূল ফাইলের লাইন আর column-এর সাথে মেলায়। Source map-এর বহুল প্রচলিত version 3 এসেছিল ২০১১ সালের দিকে, আর পরে এটি Ecma-র ECMA-426 মান হিসেবে প্রকাশিত হয়েছে।
ছোট একটি সত্যিকারের পরীক্ষা (esbuild 0.28.2). cart.js-এর applyCoupon-এ coupon undefined হলে ভুল হয়:
$ npx esbuild src/main.js --bundle --minify --sourcemap --format=esm --outfile=dist/app.min.js
dist\app.min.js 268b
dist\app.min.js.map 1.1kb
…
$ node dist/app.min.js
total 1020
…
TypeError: Cannot read properties of undefined (reading 'percent')
at r (file://~/bazarghar/dist/app.min.js:1:81)
at file://~/bazarghar/dist/app.min.js:1:219
$ node --enable-source-maps dist/app.min.js
total 1020
…
TypeError: Cannot read properties of undefined (reading 'percent')
at applyCoupon (~/bazarghar/src/cart.js:6:26)
at <anonymous> (~/bazarghar/src/main.js:6:29)
প্রথম trace-এ ফাংশনের নাম r, জায়গা লাইন 1. দ্বিতীয়টিতে applyCoupon, cart.js-এর লাইন 6. Error tracker server-এ ঠিক এই অনুবাদটি করে। Next.js-এর browser ভুলটিও একইভাবে অনুবাদ করা হলো productionBrowserSourceMaps: true দিয়ে build করে, আর source-map 0.8.0 package দিয়ে:
$ node unminify-next.mjs .next/static/chunks/0_mgxeskklrr6.js.map 1 281
0_mgxeskklrr6.js.map 1:281 -> turbopack:///[project]/app/checkout/page.tsx:6:23
console.log(order.total); // TypeError inside an event handler
Source map প্রকাশ্যে মানে কোড প্রকাশ্যে। একই run-এ curl করে দেখা গেল /_next/static/chunks/0_mgxeskklrr6.js.map উত্তর দিচ্ছে 200. ফাইলে sourcesContent-এ পুরো মূল কোড, মন্তব্যসহ। অনেক দল তাই build-এর পরে map ফাইল error tracker-এ upload করে, আর deploy করা ফোল্ডার থেকে মুছে দেয়। প্রতিটি টুল আর @sentry/nextjs-এর মতো plugin-এর docs-এ এর নিজস্ব পথ আছে। Release-এর নাম আর map-এর release এক না হলে অনুবাদ ব্যর্থ হয়; এটি সবচেয়ে সাধারণ ভুল।
Grouping আর fingerprint
একটি জনপ্রিয় পাতায় একটি ভুল ঘণ্টায় হাজার বার ঘটতে পারে। হাজারটি আলাদা alert কোনো কাজের নয়। তাই tracker প্রতিটি event-এর একটি fingerprint বানায়, আর একই fingerprint-এর event এক issue-তে জমা হয়। কঠিন অংশ হলো “একই” ঠিক করা: order 13 আর order 2071-এর ভুল একই, কিন্তু বার্তা আলাদা। নতুন deploy-এ chunk-এর নাম বদলায়, কিন্তু ভুল একই। নিচের মডেলটি এই স্বাভাবিক করার কাজটি দেখায়।
const { createHash } = require("node:crypto");
// এক ধরনের ভুলকে এক চেহারায় আনা: সংখ্যা, id, ফোন, chunk hash বাদ
function normaliseMessage(msg) {
return msg
.replace(/\b01[3-9]\d{8}\b/g, "<phone>")
.replace(/\b[0-9a-f]{8}-[0-9a-f-]{27}\b/gi, "<uuid>")
.replace(/\d+/g, "<n>");
}
function normaliseFrame(frame) { // "at f (x/ab12.js:1:9)" -> "f x/*.js"
const m = frame.match(/at (\S+) \((.*?):\d+:\d+\)/);
if (!m) return frame;
return `${m[1]} ${m[2].replace(/\/[\w-]+\.js$/, "/*.js")}`;
}
function fingerprint(ev) {
const key = [ev.type, normaliseMessage(ev.message), ...ev.frames.slice(0, 2).map(normaliseFrame)].join("|");
return createHash("sha1").update(key).digest("hex").slice(0, 8);
}
const events = [
{ type: "TypeError", message: "Cannot read properties of null (reading 'total')", frames: ["at payNow (chunks/0e0yfs38.js:1:281)"] },
{ type: "TypeError", message: "Cannot read properties of null (reading 'total')", frames: ["at payNow (chunks/9kq2ab11.js:1:290)"] },
{ type: "Error", message: "stock lookup failed for order 13", frames: ["at OrderPage (server/page.js:1:1610)"] },
{ type: "Error", message: "stock lookup failed for order 2071", frames: ["at OrderPage (server/page.js:1:1610)"] },
{ type: "Error", message: "OTP send failed for 01712345678", frames: ["at sendOtp (server/otp.js:4:9)"] },
{ type: "Error", message: "OTP send failed for 01898765432", frames: ["at sendOtp (server/otp.js:4:9)"] },
];
const issues = new Map();
for (const ev of events) {
const fp = fingerprint(ev);
const issue = issues.get(fp) ?? { title: `${ev.type}: ${normaliseMessage(ev.message)}`, count: 0 };
issue.count++;
issues.set(fp, issue);
}
console.log(`raw messages: ${new Set(events.map((e) => e.message)).size}, issues: ${issues.size}`);
for (const [fp, i] of issues) console.log(fp, `x${i.count}`, i.title);আউটপুট:
raw messages: 5, issues: 3
ecff402e x2 TypeError: Cannot read properties of null (reading 'total')
31edce1a x2 Error: stock lookup failed for order <n>
923f4818 x2 Error: OTP send failed for <phone>
ছয়টি event-এ পাঁচটি আলাদা বার্তা, কিন্তু issue মাত্র তিনটি। প্রথম দুটির বার্তা এক, তবে chunk আর column আলাদা, কারণ দ্বিতীয়টি নতুন deploy-এর; /*.js আর লাইন-column বাদ দেওয়ায় দুটি এক হলো। Order নম্বর <n> হলো, ফোন নম্বর <phone>. শেষ লাভটি দ্বিগুণ: issue-এর শিরোনামে ব্যক্তিগত তথ্যও আর নেই। আসল টুলে frame-এর গুরুত্ব আরও সূক্ষ্ম; সাধারণত in-app frame বেশি গোনা হয়, আর আপনি নিজের fingerprint নিয়মও দিতে পারেন।
এবার পুরো যাত্রাটি একসাথে দেখুন। প্রতিটি মান উপরের আসল run থেকে।
- Browser
payNow()TypeError - Listener
sendBeaconreport - Receiver
/api/client-errorsযাচাই, মোছা - Source map
1:281 → 6:23আসল লাইন - Issue
ecff402ex2
…ck:function(){console.log(null.total)},…
TypeError: Cannot read properties of null (reading 'total')
{ source: "window.onerror",
file: ".../chunks/0e0yfs38z2o_4.js",
line: 1, col: 281, page: "/checkout" }
size ≤ 8000 ✓ JSON ✓
fields: source message file line col page
scrubText() → log line লেখা
1:281 → app/checkout/page.tsx:6:23
console.log(order.total);
TypeError | Cannot read … 'total' | payNow chunks/*.js → ecff402e count 1 → 2 9kq2ab11.js:1:290 একই issue-তে
- Throw. ক্রেতা “পেমেন্ট করুন” চাপলেন। Minified কোডে compiler
order-কে সরাসরিnullবসিয়েছে, তাইnull.totalপড়তে গিয়ে TypeError. এটি event handler-এ, তাইerror.tsxএকে ধরবে না। - Listener ধরল।
window-এরerrorevent থেকে বার্তা, ফাইল, লাইন 1 আর column 281 নিয়েsendBeaconপাঠাল। পাতা বন্ধ হয়ে গেলেও beacon যায়। - Receiver যাচাই করল। ৮,০০০ অক্ষরের বেশি হলে 413, ভাঙা JSON হলে 400. শুধু সাতটি চেনা field রইল, string-এ ফোন বা token থাকলে ঢাকা পড়ল, তারপর 204.
- Source map অনুবাদ করল। 1:281 হলো
app/checkout/page.tsx-এর লাইন 6, column 23:console.log(order.total). এখন মানুষ বুঝতে পারেন কোথায় ঠিক করতে হবে। - এক issue-তে জমা। স্বাভাবিক করা চাবি থেকে fingerprint
ecff402e. পরের deploy-এ chunk-এর নাম9kq2ab11.js, column 290, তবু একই issue; count 1 থেকে 2. নতুন issue না হওয়ায় নতুন alert-ও যায়নি।
৫ · পাঠানোর আগে মুছুন — ব্যক্তিগত তথ্য আর log পাঠানো
Error report একটি ছোট ফাঁদ। ভুলের মুহূর্তে যা হাতে থাকে, সবই পাঠাতে ইচ্ছা করে: request body, header, ব্যবহারকারীর তথ্য। আর সেগুলোই সবচেয়ে বিপজ্জনক। পর্ব ৩১-এ log-এর জন্য যে নিয়ম শিখেছিলেন, এখানে আরও কড়া, কারণ ডেটা আপনার server ছেড়ে একটি তৃতীয় পক্ষের সেবায় যাচ্ছে। ক্রেতার ফোন নম্বর, ঠিকানা, OTP, token, card-এর তথ্য, এসব PII বা গোপন মান কখনো যাবে না।
নিয়ম দুই স্তরের। প্রথমে allowlist: শুধু যে field দরকার, সেগুলোই পাঠান, যেমন আমাদের receiver-এর ALLOWED. তারপর scrubber, শেষ বেড়া হিসেবে: চেনা key মুছে দেওয়া, আর লেখার ভেতরের pattern ঢেকে দেওয়া। নিচের মডেলে দুটিই।
// event পাঠানোর আগে ব্যক্তিগত তথ্য মুছে ফেলা
const DENY_KEYS = /^(password|token|authorization|cookie|address|otp)$/i;
const PATTERNS = [
[/\b01[3-9]\d{8}\b/g, "[phone]"],
[/[\w.+-]+@[\w-]+\.[\w.]+/g, "[email]"],
[/Bearer\s+[\w.-]+/g, "Bearer [token]"],
];
function scrub(value, key = "") {
if (DENY_KEYS.test(key)) return "[removed]";
if (typeof value === "string") {
return PATTERNS.reduce((s, [re, mask]) => s.replace(re, mask), value);
}
if (Array.isArray(value)) return value.map((v) => scrub(v));
if (value && typeof value === "object") {
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, scrub(v, k)]));
}
return value;
}
const event = {
message: "OTP send failed for 01712345678",
user: { id: "u_8421", email: "rahim@example.com" },
request: {
url: "/api/checkout",
headers: { authorization: "Bearer example-token-123", "user-agent": "Edge" },
body: { address: "House 12, Road 5, Rajshahi", note: "call 01898765432 first" },
},
breadcrumbs: ["login ok for rahim@example.com", "cart: product 77"],
};
console.log(JSON.stringify(scrub(event), null, 1));আউটপুটের মূল অংশ: "message": "OTP send failed for [phone]", "email": "[email]", "authorization": "[removed]", "address": "[removed]", "note": "call [phone] first", আর breadcrumb-এ "login ok for [email]". "id": "u_8421" আর "user-agent": "Edge" অক্ষত, কারণ এরা কোনো নিয়মে পড়ে না। লক্ষ করুন, ঠিকানা ধরা পড়েছে শুধু key-এর নামে; কেউ ঠিকানা note-এ লিখলে কোনো regex তা চিনত না। তাই scrubber শেষ বেড়া, প্রথম বেড়া হলো পুরো body না পাঠানো।
মনে রাখার নিয়ম
Scrubbing হবে পাঠানোর আগে, আপনার কোডে (SDK-র beforeSend বা নিজের receiver-এ)। Hosted টুলের server-side scrubbing ভালো বাড়তি বেড়া, কিন্তু তখন ডেটা ইতিমধ্যে আপনার হাত ছেড়েছে।
Session replay বা স্ক্রিন রেকর্ডের মতো সুবিধা অনেক টুলে আছে। সেগুলো চালু করলে form-এর লেখা, ঠিকানা, এমনকি পাসওয়ার্ডের ঘরও রেকর্ডে আসতে পারে, যদি mask ঠিকমতো না থাকে। চালুর আগে প্রতিটি input-এর mask যাচাই করুন, আর ব্যবহারকারীর গোপনীয়তা নীতিতে বিষয়টি জানান।
Log পাঠানো আর খোঁজা — এক লাইনে
পর্ব ৩১-এর JSON log stdout-এ যায়। একাধিক server হলে একটি agent সেগুলো সংগ্রহ করে এক জায়গায় পাঠায়, একে বলে log shipping. তারপর requestId বা trace_id দিয়ে খোঁজা যায়। Fluent Bit, Vector-এর মতো agent আর Loki, OpenSearch-এর মতো store নিয়ে আসল run আছে Cloud & DevOps কোর্সের Centralized logging পর্বে; এখানে শুধু একটি কথা: error report-এ requestId বা trace_id রাখুন, যাতে issue থেকে এক ক্লিকে ওই request-এর সব log খোলা যায়।
৬ · Metrics — prom-client আর চারটি golden signal
পর্ব ৩১-এ request-এর counter আর histogram বসিয়েছিলেন। এবার একই Express 5 API-তে আরও তিনটি: ব্যবসার দুটি counter আর saturation-এর একটি gauge. সাথে collectDefaultMetrics, যা Node process-এর memory আর event loop-এর মাপ দেয়। এখানে আসল database PostgreSQL (embedded-postgres-এর লোকাল server, port 47832).
client.collectDefaultMetrics({ register: registry });
const ordersTotal = new client.Counter({
name: 'bazarghar_orders_total', help: 'Orders placed', registers: [registry],
});
const paymentFailures = new client.Counter({
name: 'bazarghar_payment_failures_total', help: 'Failed payments',
labelNames: ['gateway'], registers: [registry],
});
const dbPoolInUse = new client.Gauge({
name: 'bazarghar_db_pool_in_use', help: 'DB connections in use', registers: [registry],
collect() { this.set(db.totalCount - db.idleCount); }, // scrape-এর মুহূর্তে মাপা
});সত্যিকারের run (prom-client 15.1.3, Express 5.3.0, pg 8.23.1). ৯টি সফল order, ২টি payment ব্যর্থ (502), আর একটি অচেনা URL ডাকার পরে GET /metrics-এর বাছাই করা লাইন (মোট ২০৯ লাইন):
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 77877248
# TYPE nodejs_eventloop_lag_p99_seconds gauge
nodejs_eventloop_lag_p99_seconds 0.030654463
# TYPE http_requests_total counter
http_requests_total{method="POST",route="/orders",status="201"} 9
http_requests_total{method="POST",route="/orders",status="502"} 2
http_requests_total{method="GET",route="unmatched",status="404"} 1
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05",route="/orders"} 11
…
http_request_duration_seconds_sum{route="/orders"} 0.10675339999999998
http_request_duration_seconds_count{route="/orders"} 11
# TYPE bazarghar_orders_total counter
bazarghar_orders_total 9
# TYPE bazarghar_payment_failures_total counter
bazarghar_payment_failures_total{gateway="example-gateway"} 2
# TYPE bazarghar_db_pool_in_use gauge
bazarghar_db_pool_in_use 0
এই কয়েক লাইন থেকেই চারটি signal পড়া যায় (একটি লোকাল run, সংখ্যা সাধারণ ফল নয়):
Errors: 2 ÷ 11 ≈ 18.2% (5xx)
Latency (গড়): 0.1068 ÷ 11 ≈ 0.0097 s ≈ 9.7 ms
Saturation: pool-এ ব্যস্ত সংযোগ 0, event loop lag p99 ≈ 31 ms
সতর্কতা দুটি। এক, গড় latency প্রায়ই মিথ্যা বলে; ধীরতম ৫% ক্রেতার কষ্ট গড়ে ঢাকা পড়ে। তাই percentile দেখুন, যা Prometheus histogram-এর bucket থেকে histogram_quantile দিয়ে হিসাব করে। দুই, /metrics শুধু “এখন পর্যন্ত মোট” দেয়; “প্রতি মিনিটে কত” বের করে Prometheus নিজে, বারবার পড়ে (rate())। PromQL, scrape আর bucket বাছাইয়ের পূর্ণ আলোচনা আছে Metrics ও Prometheus পর্বে।
Payment-এর label-এ gateway-এর নাম ঠিক আছে, কারণ মান কয়েকটি মাত্র। কিন্তু userId, orderId, ফোন নম্বর কখনো label নয়: প্রতিটি মান একটি নতুন সংখ্যার ধারা, অর্থাৎ label cardinality-র বিস্ফোরণ, আর metric-এ ব্যক্তিগত তথ্য। আমাদের অচেনা URL-টিও route="unmatched" হয়েছে, আসল path নয়; নইলে আক্রমণকারী এলোমেলো URL পাঠিয়েই আপনার metric store ভরে দিতে পারত।
৭ · Tracing — একটি ক্লিক থেকে database query পর্যন্ত
Metric বলে “/orders ধীর”, log বলে “এই request-এ কী হলো”। কিন্তু কোন অংশে সময় গেল: browser-এ, network-এ, API-র কোডে, না database-এ? Distributed tracing এর উত্তর দেয়। প্রতিটি কাজের অংশ একটি span, সবার একই trace id, আর প্রতিটির একটি parent. ধারণা, sampling আর collector-এর পূর্ণ আলোচনা Tracing ও OpenTelemetry পর্বে; এখানে আমাদের অ্যাপে বসানো।
কুরিয়ারে একটি পার্সেলের ট্র্যাকিং নম্বর ভাবুন। ঢাকার অফিস, ট্রাক, রাজশাহীর হাব, ডেলিভারিম্যান, প্রত্যেকে নিজের ধাপের শুরু আর শেষের সময় লেখেন, একই নম্বরে। শেষে দেখা যায় পার্সেলটি হাবে ৬ ঘণ্টা পড়ে ছিল। Trace id সেই নম্বর, আর প্রতিটি ধাপের লেখা একটি span.
লেখার সময়ের version: @opentelemetry/sdk-node 0.300.0 (যা SDK 3.0.0 ব্যবহার করে), @opentelemetry/api 1.10.0, instrumentation-http 0.300.0, instrumentation-pg 0.77.0. SDK সবার আগে load হতে হবে, তাই আলাদা ফাইলে রেখে node --require ./tracing.cjs api.cjs দিয়ে চালানো।
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { SimpleSpanProcessor, ConsoleSpanExporter } = require('@opentelemetry/sdk-trace');
const { envDetector } = require('@opentelemetry/resources');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { PgInstrumentation } = require('@opentelemetry/instrumentation-pg');
const sdk = new NodeSDK({
resourceDetectors: [envDetector], // host আর process detector বন্ধ
spanProcessors: [new SimpleSpanProcessor({ exporter: new ConsoleSpanExporter() })],
instrumentations: [
new HttpInstrumentation({
ignoreIncomingRequestHook: (req) => ['/metrics', '/healthz', '/readyz'].includes(req.url),
}),
new PgInstrumentation(),
],
});
sdk.start();আসল run-এ দুটি ফাঁদ। প্রথম চেষ্টায় পুরোনো অভ্যাসে লেখা হয়েছিল new SimpleSpanProcessor(new ConsoleSpanExporter()). Server চলল, request সফল, কিন্তু একটিও span ছাপা হলো না। OTEL_LOG_LEVEL=debug দিয়ে দেখা গেল ভেতরে TypeError: Cannot read properties of undefined (reading 'export'): SDK 3.0-তে processor একটি option object নেয়, { exporter }. অর্থাৎ ভুল setup-এ monitoring নিজেই চুপচাপ বন্ধ থাকে। দ্বিতীয়টি: sdk-trace-node 2.12.0 আর api 1.10.0 একসাথে install করতে গেলে npm দিল ERESOLVE, কারণ সেটির peer চাহিদা <1.10.0. OpenTelemetry-র package-গুলো একই release ধারার রাখুন।
এবার browser-এর ভূমিকায় একটি ছোট script: একটি “click” span খোলে, W3C-র traceparent header বানায়, আর API-কে order পাঠায়। আসল browser-এ এই কাজ করে OpenTelemetry-র web SDK বা error tracker-এর browser SDK; এখানে একই propagator সরাসরি ডাকা হয়েছে।
const click = tracer.startSpan('click: place-order button');
const ctx = otel.trace.setSpan(otel.context.active(), click);
const headers = { 'content-type': 'application/json' };
propagator.inject(ctx, headers, otel.defaultTextMapSetter);
console.log('traceparent:', headers.traceparent);
await fetch('http://127.0.0.1:47833/orders', { method: 'POST', headers, body: '{"productId":77,"qty":2}' });
click.end();সত্যিকারের run. প্রতিটি span-এর জন্য ConsoleSpanExporter-এর বড় object-এর পাশে একটি ছোট লাইনও ছাপা হয়েছে, একটি দ্বিতীয় exporter দিয়ে, পড়ার সুবিধায়। প্রথমে browser-এর ভূমিকার script-এর আউটপুট:
traceparent: 00-ae19b26289627244de439ff283018ba8-9a2a043c054a23f0-01
status 201 {"id":4}
SPAN click: place-order button trace=ae19b262 span=9a2a043c054a23f0 parent=- 83.7ms
আর একই সময়ে API process-এর আউটপুট (span শেষ হওয়ার ক্রমে, তাই ভেতরেরগুলো আগে):
SPAN pg.connect trace=ae19b262 span=b1ad69de42b5f4f0 parent=d5e5a8f88b70a07d 37.9ms
SPAN pg-pool.connect trace=ae19b262 span=d5e5a8f88b70a07d parent=c9978f356041292d 39.6ms
SPAN pg.query:INSERT bazarghar_mon trace=ae19b262 span=bbe9189129b917a1 parent=c9978f356041292d 2.5ms
SPAN orders.create trace=ae19b262 span=c9978f356041292d parent=6c243e173f488595 43.5ms
SPAN POST trace=ae19b262 span=6c243e173f488595 parent=9a2a043c054a23f0 54.5ms
আর ConsoleSpanExporter-এর DB span-এর অংশ:
name: 'pg.query:INSERT bazarghar_mon',
attributes: {
'db.system.name': 'postgresql',
'db.namespace': 'bazarghar_mon',
'server.port': 47832,
'db.query.text': 'INSERT INTO orders (product_id, qty) VALUES ($1, $2) RETURNING id'
}
Query-র লেখায় $1, $2, আসল মান নয়। Parameterized query নিরাপত্তার সাথে সাথে trace-কেও পরিষ্কার রাখে: কোনো ক্রেতার তথ্য span-এ যায়নি।
- click
9a2a043c…83.7 ms - POST
6c243e17…54.5 ms - orders.create
c9978f35…43.5 ms - pg
connect + INSERT39.6 + 2.5 ms
traceparent: 00-ae19b262…-9a2a043c054a23f0-01 version - trace id - parent span id - flags
POST trace=ae19b262
parent=9a2a043c054a23f0 (isRemote: true)
url.path: /orders status: 201
orders.create trace=ae19b262
parent=6c243e173f488595
order.product_id: 77
pg-pool.connect 39.6 ms ← সময়ের বেশিরভাগ
pg.query:INSERT 2.5 ms
db.query.text: INSERT … VALUES ($1, $2)
click 83.7 = POST 54.5 + বাইরের 29.2
POST 54.5 = orders.create 43.5 + বাকি 11.0
orders.create 43.5 ≈ pool.connect 39.6 + INSERT 2.5
- ক্লিক, শিকলের মাথা। Click span-এর কোনো parent নেই। তার trace id আর নিজের span id
traceparentheader-এ লিখে request-এর সাথে পাঠানো হলো। - অন্য process, একই trace. API-র http instrumentation header পড়ে server span খুলল। Trace id একই
ae19b262…, আর parent হলো click span9a2a043c054a23f0;isRemote: trueবলে parent অন্য process-এর। - কোডের নিজের ধাপ। Service-এ
startActiveSpan('orders.create')একটি span খুলল, parent হলো POST span. Attribute-এ শুধু product id, কোনো ব্যক্তিগত তথ্য নয়। - Database. pg instrumentation দুটি span দিল: pool থেকে সংযোগ পেতে 39.6 ms, আর INSERT নিজে মাত্র 2.5 ms. প্রথম request বলে pool-কে নতুন সংযোগ খুলতে হয়েছে।
- কোথায় সময় গেল। 83.7 ms-এর মধ্যে 39.6 ms গেছে সংযোগ খোলায়, query-তে নয়। Query optimize করে লাভ হতো না; সমাধান হলো pool আগে থেকে গরম রাখা। Trace ছাড়া এই সিদ্ধান্ত অনুমান হতো।
Trace আর error-ও জোড়া লাগে। আগের Sentry run-এর envelope header-এ trace_id ছিল। Error tracker আর trace backend একই trace id দেখালে, একটি ভুলের issue থেকে সেই request-এর পুরো trace খোলা যায়। তাই tracing বসানোর সময় error-এর SDK-র সাথে সামঞ্জস্য মিলিয়ে নিন; কিছু SDK নিজেই OpenTelemetry-র ওপর তৈরি।
৮ · বাইরে থেকে দেখা — uptime, synthetic check, RUM আর status page
সব monitoring যদি আপনার server-এর ভেতরে থাকে, তাহলে server পুরো বন্ধ হলে কে খবর দেবে? DNS ভুল, TLS certificate-এর মেয়াদ শেষ, hosting-এর network সমস্যা, এগুলো ভেতর থেকে দেখা যায় না। তাই একটি uptime check বাইরে থেকে নির্দিষ্ট সময় পরপর আপনার /healthz বা মূল পাতা ডাকে, আর একাধিক জায়গা থেকে ডাকলে একটি জায়গার network সমস্যাকে আপনার সমস্যা ভেবে ভুল হয় না।
নিচের script সত্যিই চালানো হয়েছে পর্ব ৩১-এর মতো /healthz আর /readyz-ওয়ালা আমাদের API-র বিরুদ্ধে। প্রতিটি check-এর নিজের timeout, কারণ উত্তর না আসাও একটি উত্তর।
const base = process.argv[2];
const checks = [
{ name: 'liveness', path: '/healthz', expect: 200, timeoutMs: 2000 },
{ name: 'readiness', path: '/readyz', expect: 200, timeoutMs: 2000 },
];
async function runCheck(c) {
const started = performance.now();
try {
const res = await fetch(base + c.path, { signal: AbortSignal.timeout(c.timeoutMs) });
const ms = Math.round(performance.now() - started);
return { name: c.name, ok: res.status === c.expect, detail: `HTTP ${res.status} in ${ms} ms` };
} catch (err) {
const ms = Math.round(performance.now() - started);
const why = err.name === 'TimeoutError' ? 'timeout' : (err.cause?.code ?? err.name);
return { name: c.name, ok: false, detail: `${why} after ${ms} ms` };
}
}
const results = await Promise.all(checks.map(runCheck));
for (const r of results) console.log(`${r.ok ? 'UP ' : 'DOWN'} ${r.name.padEnd(9)} ${r.detail}`);
process.exitCode = results.every((r) => r.ok) ? 0 : 1;সত্যিকারের run, তিন অবস্থায়: স্বাভাবিক; /readyz-কে ইচ্ছা করে ৩ সেকেন্ড ধীর করে; আর এমন port-এ যেখানে কিছু চলছে না:
$ node uptime-check.mjs http://127.0.0.1:47833
UP liveness HTTP 200 in 26 ms
UP readiness HTTP 200 in 42 ms
exit 0
$ node uptime-check.mjs http://127.0.0.1:47833 # readyz এখন ৩ সেকেন্ড ধীর
UP liveness HTTP 200 in 30 ms
DOWN readiness timeout after 2021 ms
exit 1
$ node uptime-check.mjs http://127.0.0.1:47899 # কিছু চলছে না
DOWN liveness ECONNREFUSED after 24 ms
DOWN readiness ECONNREFUSED after 3 ms
exit 1
দ্বিতীয় অবস্থাটিই সবচেয়ে শিক্ষণীয়: process বেঁচে আছে, কিন্তু ক্রেতার কাছে সাইট “ঘুরছে”। Timeout না থাকলে check নিজেই আটকে থাকত। Exit code 0 বা 1 হওয়ায় script-টি cron, CI বা যেকোনো scheduler থেকে চালানো যায়।
হিসাব: কত দ্রুত জানবেন
ধরে নেওয়া সংখ্যা: প্রতি ৬০ সেকেন্ডে check, পরপর ২ বার ব্যর্থ হলে alert, timeout ১০ সেকেন্ড। সবচেয়ে খারাপ ক্ষেত্রে সমস্যা শুরু হয় একটি সফল check-এর ঠিক পরে।
দ্বিতীয় ব্যর্থ check: আরও 60 s → 130 s ≈ ২ মিনিট ১০ সেকেন্ড
check প্রতি ৩০ s হলে: 30 + 10 + 30 = 70 s
দ্রুত জানতে check ঘন করলে খরচ আর চাপ বাড়ে, আর “পরপর ২ বার” নিয়ম না থাকলে একটি ক্ষণিকের ঝাঁকুনিতেই রাতে ফোন বাজবে।
Synthetic check: ব্যবহারকারীর মতো হাঁটা
/healthz সবুজ হলেও checkout ভাঙা থাকতে পারে। Synthetic monitoring একটি script দিয়ে নিয়মিত আসল পথে হাঁটে: হোম পাতা, পণ্য খোঁজা, cart-এ যোগ, checkout পাতা পর্যন্ত। পর্ব ৭৬-এর Playwright test প্রায় এভাবেই production-এর বিরুদ্ধে নির্দিষ্ট সময় পরপর চালানো যায়। দুটি সাবধানতা: আসল order বা আসল payment করবেন না (test account আর test mode রাখুন), আর synthetic traffic-কে metric-এ চিহ্নিত রাখুন, যাতে ব্যবসার হিসাবে না মেশে।
RUM: আসল ক্রেতার গতি
Lab-এর মাপ আপনার দ্রুত ল্যাপটপের; ক্রেতা হয়তো গ্রামে পুরোনো ফোনে মোবাইল ডেটায়। RUM আসল browser থেকে মাপ আনে। Google Chrome দলের web-vitals library (লেখার সময় 6.2.3) onLCP, onINP, onCLS দিয়ে Core Web Vitals দেয়; আপনি sendBeacon-এ নিজের endpoint-এ পাঠান, ঠিক আমাদের error report-এর মতো। কোন মান ভালো, কীভাবে উন্নতি করবেন, সেটি পরের পর্ব ৮৪ (Web performance)-এর বিষয়।
Status page
সমস্যার সময় ক্রেতারা জানতে চান: “শুধু আমার, না সবার?” একটি status page এই প্রশ্নের উত্তর দেয়, আর আপনার ফোন ও inbox-কে রেহাই দেয়। দুটি নিয়ম: এটি মূল সাইটের থেকে আলাদা পরিকাঠামোয় রাখুন (একই server পড়লে status page-ও পড়বে), আর সত্য লিখুন, সময়সহ। Incident চলাকালীন আপডেটের ছন্দ নিয়ে আছে Incident management পর্বে। Uptime check আর status page-এর অনেক hosted সেবা আছে; কোনটি নেবেন, তাদের বর্তমান শর্ত দেখে ঠিক করুন।
৯ · Alert যা কাজের — SLO, error budget, burn rate আর on-call
সব কিছু মাপা যায়, কিন্তু সব কিছুতে মানুষকে ডাকা যায় না। ভালো alert-এর মূলনীতি: লক্ষণে alert, কারণে নয়। “CPU 85%” একটি কারণ; ক্রেতার কোনো ক্ষতি না-ও হতে পারে। “Checkout-এর ৫% request ব্যর্থ” একটি লক্ষণ; ক্ষতি নিশ্চিত। কারণগুলো dashboard-এ থাকুক, ভুল খোঁজার সময় কাজে লাগবে।
SLI, SLO আর error budget
SLI একটি মাপ, যেমন “সফল checkout request-এর অনুপাত”। SLO তার লক্ষ্য, যেমন “৩০ দিনে ৯৯.৯%”. বাকি ০.১% হলো error budget: এতটুকু ব্যর্থতা মেনে নেওয়া হলো। ১০০% লক্ষ্য কেন নয়, সেটি ভাবনার প্রশ্নে আছে; পুরো তত্ত্ব Alerting ও SLO পর্বে।
// SLO থেকে error budget: সময়ের হিসাব আর request-এর হিসাব
function errorBudget({ slo, windowDays, expectedRequests, goodSoFar, badSoFar, daysPassed }) {
const allowedBadRatio = 1 - slo;
const budgetMinutes = windowDays * 24 * 60 * allowedBadRatio;
const budgetRequests = Math.round(expectedRequests * allowedBadRatio);
const total = goodSoFar + badSoFar;
const used = badSoFar / budgetRequests;
// এই গতিতে চললে মাস শেষে কত খরচ হবে
const projected = used * (windowDays / daysPassed);
return {
budgetMinutes: budgetMinutes.toFixed(1),
budgetRequests,
successSoFar: ((goodSoFar / total) * 100).toFixed(3) + "%",
usedPercent: (used * 100).toFixed(1) + "%",
leftRequests: budgetRequests - badSoFar,
projectedAtEnd: (projected * 100).toFixed(0) + "%",
};
}
// বাজারঘর checkout API: SLO 99.9%, ৩০ দিনের জানালা, মাসে ধরে নেওয়া ৪২ লাখ request
const base = { slo: 0.999, windowDays: 30, expectedRequests: 4_200_000 };
const calm = { ...base, goodSoFar: 1_678_320, badSoFar: 1_680, daysPassed: 12 };
const bad = { ...base, goodSoFar: 1_676_850, badSoFar: 3_150, daysPassed: 12 };
console.log("calm month:", JSON.stringify(errorBudget(calm)));
console.log("bad month: ", JSON.stringify(errorBudget(bad)));
console.log("99.99% SLO:", JSON.stringify(errorBudget({ ...calm, slo: 0.9999 })));আউটপুট:
calm month: {"budgetMinutes":"43.2","budgetRequests":4200,"successSoFar":"99.900%","usedPercent":"40.0%","leftRequests":2520,"projectedAtEnd":"100%"}
bad month: {"budgetMinutes":"43.2","budgetRequests":4200,"successSoFar":"99.813%","usedPercent":"75.0%","leftRequests":1050,"projectedAtEnd":"188%"}
99.99% SLO: {"budgetMinutes":"4.3","budgetRequests":420,"successSoFar":"99.900%","usedPercent":"400.0%","leftRequests":-1260,"projectedAtEnd":"1000%"}
৯৯.৯% মানে ৩০ দিনে ৪৩.২ মিনিট পুরো বন্ধ থাকার সমান, বা ৪২ লাখের মধ্যে ৪,২০০টি ব্যর্থ request. শান্ত মাসে ১২ দিনে ৪০% খরচ, ঠিক সময়ের সমান গতিতে (১২ ÷ ৩০ = ৪০%), তাই মাস শেষে ১০০%: সীমার কিনারায়, কিন্তু ঠিক আছে। খারাপ মাসে একই সময়ে ৭৫%, এই গতিতে মাস শেষে ১৮৮%, অর্থাৎ নতুন feature-এর ঝুঁকি কমিয়ে স্থিতিতে মন দেওয়ার সময়। আর একই ফলকে ৯৯.৯৯% লক্ষ্যে মাপলে budget মাত্র ৪২০, ব্যবহার ৪০০%: লক্ষ্য বেশি উঁচু হলে সাধারণ মাসও “ব্যর্থ”।
Burn rate: কত দ্রুত পুড়ছে
Burn rate = বর্তমান error হার ÷ অনুমোদিত error হার। ৯৯.৯% SLO-তে অনুমোদিত হার ০.১%; এখন ৩% ভুল হলে burn rate ৩০, অর্থাৎ ৩০ দিনের budget ১ দিনে শেষ। Google-এর SRE Workbook-এ একটি পরিচিত নিয়ম আছে: ১ ঘণ্টার জানালায় burn rate ১৪.৪-এর বেশি হলে page, কারণ এই গতিতে এক ঘণ্টায় মাসের budget-এর ২% পুড়ে যায় (14.4 × 1 ÷ 720 = 0.02). সাথে একটি ছোট ৫ মিনিটের জানালাও দেখা হয়, যাতে সমস্যা থামলে alert দ্রুত থামে। নিচের মডেলে এক ঘণ্টার নকল traffic, মিনিট ১৫ থেকে ৩৪ পর্যন্ত payment gateway-এর সমস্যায় ৩% ভুল।
// এক ঘণ্টার নকল traffic, প্রতি মিনিটে burn rate দেখে alert-এর সিদ্ধান্ত
let seed = 83;
const rand = () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648;
const SLO = 0.999, BUDGET = 1 - SLO, THRESHOLD = 14.4;
const minutes = [];
for (let m = 0; m < 60; m++) {
const requests = 900 + Math.floor(rand() * 200);
const badRatio = m >= 15 && m < 35 ? 0.03 : 0.0005; // মিনিট ১৫-৩৪: gateway সমস্যা
minutes.push({ requests, bad: Math.round(requests * badRatio) });
}
function burn(upTo, size) {
const win = minutes.slice(Math.max(0, upTo - size + 1), upTo + 1);
const req = win.reduce((s, x) => s + x.requests, 0);
const bad = win.reduce((s, x) => s + x.bad, 0);
return bad / req / BUDGET;
}
let firing = false;
for (let m = 0; m < 60; m++) {
const long = burn(m, 60), short = burn(m, 5);
const shouldFire = long > THRESHOLD && short > THRESHOLD;
if (shouldFire !== firing) {
firing = shouldFire;
console.log(`min ${m}: ${firing ? "FIRING " : "RESOLVED"} long=${long.toFixed(1)} short=${short.toFixed(1)}`);
}
if (m === 14 || m === 20 || m === 59) {
console.log(`min ${m}: (check) long=${long.toFixed(1)} short=${short.toFixed(1)}`);
}
}
const total = minutes.reduce((s, x) => s + x.bad, 0);
console.log(`bad requests this hour: ${total}, monthly budget at 4,200,000 req: 4200`);আউটপুট:
min 14: (check) long=0.5 short=0.6
min 20: (check) long=8.8 short=30.0
min 28: FIRING long=14.7 short=30.0
min 37: RESOLVED long=15.9 short=11.7
min 59: (check) long=10.2 short=0.6
bad requests this hour: 616, monthly budget at 4,200,000 req: 4200
সমস্যা শুরু মিনিট ১৫-তে, alert বাজল মিনিট ২৮-এ। মিনিট ২০-এ ছোট জানালা ৩০.০ হলেও লম্বা জানালা মাত্র ৮.৮; একা ছোট জানালা দেখলে প্রতিটি ক্ষণিকের ঝাঁকুনিতে page হতো। সমস্যা থামে মিনিট ৩৫-এ, আর alert থামে মিনিট ৩৭-এ, যদিও লম্বা জানালা তখনও ১৫.৯. শুধু লম্বা জানালার নিয়মে একই ডেটায় (শর্ত থেকে && short > THRESHOLD বাদ দিয়ে চালানো হয়েছে) alert থামত মিনিট ৪২-এ। খেয়াল করুন, এই নকলে শুরুর দিকে লম্বা জানালায় ৬০ মিনিটের কম ডেটা থাকে। এক ঘণ্টায় ৬১৬টি ব্যর্থ request, মাসের budget-এর প্রায় ১৪.৭%.
1long = burn(last 60 min)2short = burn(last 5 min)3if long > 14.4 and4 short > 14.4:5 FIRING → page on-call6else if firing:7 RESOLVED → notify
| মিনিট | |
| request / bad | |
| long burn | |
| short burn | |
| alert |
- শান্ত সময়। মিনিট ১৪-এ ৯১৪টি request, একটিও ব্যর্থ নয়। দুই জানালার burn rate ০.৫ আর ০.৬, অর্থাৎ budget স্বাভাবিকের চেয়ে ধীরে খরচ হচ্ছে।
- সমস্যা শুরু। মিনিট ১৫-এ ৯৬৭-এর মধ্যে ২৯টি ব্যর্থ। ছোট জানালা লাফিয়ে ৬.২, লম্বা জানালা ২.২. কোনোটিই ১৪.৪ ছোঁয়নি।
- ছোট জানালা একা উঁচু। মিনিট ২০-এ ছোট জানালা ৩০.০, কিন্তু লম্বা জানালা ৮.৮. দুই শর্তই লাগে, তাই চুপ। এই শর্ত এক মিনিটের ঝাঁকুনিতে ঘুম ভাঙানো ঠেকায়।
- Alert বাজল। মিনিট ২৮-এ লম্বা জানালা ১৪.৭, ছোট ৩০.০, দুটিই ১৪.৪-এর ওপরে। On-call মানুষের কাছে page গেল, সাথে runbook-এর link.
- কাজ চলছে। মিনিট ৩৪-এ লম্বা জানালা ১৭.২-এ উঠেছে। On-call runbook ধরে gateway-এর status দেখলেন, আর payment-এর পথ অন্য gateway-এ সরালেন বা বিকল্প চালু করলেন।
- ভুল থামল, alert নয়। মিনিট ৩৫-এ মাত্র ১টি ব্যর্থ, কিন্তু ছোট জানালায় আগের মিনিটের ভুল রয়ে গেছে: ২৩.৮. লম্বা জানালা ১৬.৮.
- Alert থামল। মিনিট ৩৭-এ ছোট জানালা ১১.৭, ১৪.৪-এর নিচে, তাই RESOLVED. লম্বা জানালা ১৫.৯ হলেও থামা গেল, কারণ দুই শর্তই লাগে। শুধু লম্বা জানালা দেখলে থামত মিনিট ৪২-এ।
Alert fatigue আর ভালো alert-এর নিয়ম
যে alert-এ কিছু করার নেই, সেটি alert নয়, শব্দ। বারবার এমন শব্দ শুনলে মানুষ অভ্যস্ত হয়ে যান, তখন আসল alert-ও উপেক্ষিত হয়। একে বলে alert fatigue. ছোট দলের জন্য কয়েকটি সহজ নিয়ম:
- Page শুধু তিনটিতে: SLO-র দ্রুত burn, সাইট বাইরে থেকে বন্ধ, আর ব্যবসার বড় সংকেত (যেমন এক ঘণ্টা কোনো সফল order নেই, যখন সাধারণত শতাধিক থাকে)।
- বাকি সব ticket বা দিনের রিপোর্টে: ধীর burn, disk ৭০%, নতুন issue-এর সারাংশ।
- প্রতিটি page-এ একটি runbook: কী দেখবেন, কোন dashboard, আগে কী চেষ্টা করবেন (যেমন rollback), কাকে ডাকবেন।
- প্রতি সপ্তাহে হিসাব: কোন alert-এ কেউ কিছু করেননি? সেটি মুছুন বা ticket-এ নামান।
ছোট দলের on-call
দুই-তিনজনের দলেও on-call লাগে, কারণ ক্রেতা রাতেও কেনেন। একজনের ঘাড়ে সব চাপালে কিছুদিন পরে সেই মানুষটিই চলে যান। পালা করে সপ্তাহ ভাগ করুন, একজন প্রধান আর একজন পেছনে। রাতের page-এর পরদিন বিশ্রামের সুযোগ রাখুন। প্রতিটি বড় ঘটনার পরে দোষারোপ ছাড়া একটি ছোট লেখা (postmortem) লিখুন: কী হলো, কেন ধরা পড়তে দেরি হলো, কোন alert বা runbook বদলাতে হবে। Severity, ভূমিকা আর postmortem-এর নিয়ম পাবেন Incident management পর্বে, আর নিরাপত্তা ঘটনার জন্য Incident response পর্বে।
# Alert: checkout SLO fast burn (burn rate > 14.4, 1h ও 5m)
১. Status: error tracker-এ গত ১ ঘণ্টার নতুন issue, release অনুযায়ী
২. গত ২ ঘণ্টায় deploy হয়েছে? হলে আগে rollback, পরে খোঁজ
৩. bazarghar_payment_failures_total কোন gateway-এ বাড়ছে?
৪. Gateway-এর নিজের status page দেখুন; সমস্যা ওদের হলে বিকল্প gateway চালু
৫. Status page-এ ১৫ মিনিটের মধ্যে প্রথম খবর
৬. শেষে: incident-এর সময়, প্রভাব, budget কত খরচ হলো লিখে রাখুনআরও গভীরে: “চারটি golden signal” এসেছে Google-এর Site Reliability Engineering বইয়ের (২০১৬) “Monitoring Distributed Systems” অধ্যায় থেকে, আর multi-window, multi-burn-rate alert এসেছে The Site Reliability Workbook (২০১৮)-এর “Alerting on SLOs” অধ্যায় থেকে। Trace-এর header ঠিক করে W3C Trace Context (২০২০ সালে Recommendation). OpenTelemetry জন্মেছে ২০১৯ সালে OpenTracing আর OpenCensus মিলে, আর এখন CNCF-এর প্রকল্প। Error tracker-এর grouping-এর ভেতরের কাজ কোনো মান নয়; প্রতিটি টুলের নিজস্ব নিয়ম, যা তাদের docs-এ বর্ণনা করা থাকে।
পরীক্ষা ও ইন্টারভিউতে:
- চারটি golden signal কী? Latency, traffic, errors, saturation.
- Source map ছাড়া production-এর browser ভুলে কী সমস্যা? Stack trace minified ফাইলের লাইন 1-এর column দেখায়, নাম ছোট; আসল ফাইল ও লাইন বোঝা যায় না।
- ৯৯.৯% মাসিক SLO-তে downtime budget কত? ৩০ দিনে ৪৩.২ মিনিট।
- Burn rate ১৪.৪ মানে? Budget স্বাভাবিকের ১৪.৪ গুণ দ্রুত পুড়ছে; এক ঘণ্টায় ৩০ দিনের budget-এর ২%.
- Next.js-এ server-এর ভুল কোথায় ধরবেন?
instrumentation.ts-এরonRequestError-এ; browser-এerror.tsx-এরdigestদিয়ে জোড়া লাগান।
১০ · AI-এর যুগে monitoring
“আমার Next.js অ্যাপে error tracking যোগ করে দাও” বললে AI দ্রুত কোড দেবে। সমস্যা হলো, monitoring-এর ভুল চলার সময় চোখে পড়ে না, পড়ে ঠিক সেই রাতে, যখন monitoring-এর সবচেয়ে দরকার। এই পর্বের ল্যাবেই দেখেছেন: পুরোনো ধাঁচে লেখা একটি OpenTelemetry লাইন কোনো error ছাড়াই সব span হারিয়ে দিয়েছিল।
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
পুরোনো API: pages router-এর _error.js, error.tsx-এ শুধু reset, পুরোনো OTel processor-এর ধরন | আপনার version-এর nextjs.org আর opentelemetry.io docs মিলিয়ে নিন |
| বানানো option বা package নাম | npm view <pkg> version, আর docs-এ option খুঁজুন |
| আসল DSN বা token কোডে বসানো, বা উদাহরণে আপনার key চাওয়া | DSN env-এ রাখুন; auth token কখনো prompt-এ নয় |
পুরো req.body বা header error-এ পাঠানো | নকল ফোন নম্বর আর token দিয়ে ভুল ঘটিয়ে নিজের receiver-এ দেখুন |
| Source map প্রকাশ্যে রেখে দেওয়া | Deploy-এর পর .map URL-এ curl করে দেখুন |
| প্রতিটি metric-এ alert, CPU-তে page | প্রতিটি page-এর জন্য জিজ্ঞেস করুন: ক্রেতার কী ক্ষতি, আর আমি কী করব? |
catch {}-এ ভুল গিলে ফেলা | প্রতিটি catch পড়ুন: report, log বা আবার throw আছে কি না |
userId বা পুরো path metric label-এ | /metrics খুলে label-এর মান গুনে দেখুন |
AI-কে শর্তসহ কাজ দিন, আর যাচাইয়ের পথও চেয়ে নিন। আসল DSN, auth token বা production-এর error payload prompt-এ দেবেন না।
AI-এর লেখা monitoring কোড মেলানোর চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১লক্ষ্য কেন ৯৯.৯%, ১০০% নয়? ক্রেতার কাছে তো প্রতিটি ব্যর্থতাই খারাপ।
Error budget-এর ধারণা জনপ্রিয় করেছে Google-এর SRE চর্চা, আর ২০১৬ সালের Site Reliability Engineering বইয়ে Ben Treynor Sloss-এর দলের এই যুক্তি বিস্তারিত আছে: ১০০% প্রায় কখনো সঠিক লক্ষ্য নয়। কারণ তিনটি।
প্রথম, ক্রেতা পার্থক্যটি টের পান না। ক্রেতার নিজের ফোন, Wi-Fi, মোবাইল নেটওয়ার্ক, লোডশেডিং, এসবের নির্ভরযোগ্যতা ৯৯.৯৯%-এর অনেক নিচে। আপনার সেবা ৯৯.৯৯% থেকে ১০০%-এ নিলে ক্রেতার অভিজ্ঞতায় পার্থক্য প্রায় অদৃশ্য, কারণ বাকি পথের ব্যর্থতা ঢেকে দেয়।
দ্বিতীয়, প্রতিটি নয় যোগ করতে খরচ বহুগুণ বাড়ে। ৯৯.৯% মানে মাসে প্রায় ৪৩ মিনিট; ৯৯.৯৯% মানে প্রায় ৪.৩ মিনিট, যা একটি সাধারণ deploy-এর ভুল ধরা আর rollback করতেই লেগে যেতে পারে। তখন লাগে একাধিক region, স্বয়ংক্রিয় failover, অনেক ধীর ও সাবধানী release. একটি ছোট দোকানের জন্য সেই খরচ কি যৌক্তিক?
তৃতীয়, budget দুই দলের ঝগড়া মেটায়। Feature দল দ্রুত ছাড়তে চায়, অপারেশন দল স্থিতি চায়। Budget থাকলে প্রশ্নটি মতামতের বদলে সংখ্যার: budget বাকি থাকলে ঝুঁকি নেওয়া যায়, শেষ হলে স্থিতিতে মন। আমাদের হিসাবে “খারাপ মাস” মাস শেষে ১৮৮%-এ যাচ্ছিল; সেই সংখ্যাই সিদ্ধান্ত দেয়।
আরেকটি সূক্ষ্ম কথা: আপনার নির্ভরতাগুলোর নির্ভরযোগ্যতা গুণ হয়। Hosting ৯৯.৯৫%, database ৯৯.৯৫%, payment gateway ৯৯.৯% হলে (ধরে নেওয়া সংখ্যা, আর ব্যর্থতাগুলো স্বাধীন ধরে) checkout-এর সর্বোচ্চ সম্ভাবনা প্রায় 0.9995 × 0.9995 × 0.999 ≈ 99.8%. নিজের SLO এর চেয়ে উঁচু ঘোষণা করলে সেটি শুধু ইচ্ছা।
অবশ্য সব কাজের লক্ষ্য এক নয়। Payment-এর টাকা কাটা আর order লেখার সঠিকতায় (data integrity) আপনি “ভুল হবে না” ধরে নকশা করেন: transaction, idempotency, reconciliation. SLO মূলত availability আর latency-র জন্য।
মূল উপলব্ধি: লক্ষ্য ঠিক করুন ক্রেতার প্রয়োজন আর নির্ভরতার সীমা দেখে, আর বাকি অংশকে পরিকল্পিত ঝুঁকির বাজেট হিসেবে খরচ করুন।
প্র ০২“লক্ষণে alert, কারণে নয়” — কিন্তু disk ভরে যাওয়ার মতো কারণ আগে জানলে তো বিপদ ঠেকানো যায়। তাহলে কি কারণ-ভিত্তিক alert পুরো বাদ?
পুরো বাদ নয়, তবে তার জায়গা বদলায়। এই চিন্তার একটি পরিচিত লেখা হলো Google-এর প্রকৌশলী Rob Ewaschuk-এর “My Philosophy on Alerting” (২০১০-এর দশকের শুরুতে লেখা, পরে SRE বইয়ের অধ্যায়ে প্রভাব ফেলে)। মূল যুক্তি: কারণ অসংখ্য, লক্ষণ কয়েকটি। CPU, memory, disk, connection, queue, প্রতিটির জন্য alert লিখলে শত শত নিয়ম হয়, আর বেশিরভাগ সময় সেগুলো বাজে যখন ক্রেতার কোনো ক্ষতি হচ্ছে না। CPU ৯০% হতে পারে একটি ভারী কিন্তু স্বাভাবিক কাজে। আবার এমন কারণে সাইট ভাঙতে পারে যার জন্য কেউ alert লেখেননি। লক্ষণ-ভিত্তিক alert দুটি সমস্যাই সমাধান করে: যে কারণেই হোক, ক্রেতা কষ্ট পেলে বাজবে।
তবে একটি ব্যতিক্রম বইটিতেই স্বীকৃত: যে কারণ নিশ্চিতভাবে ও শিগগির লক্ষণ হয়ে দাঁড়াবে, আর আগে জানলে সহজে ঠেকানো যায়। Disk এই শ্রেণির ভালো উদাহরণ। PostgreSQL-এর disk ভরে গেলে লেখা থেমে যাবে; সেটি নিশ্চিত। তাই এখানে কারণ-ভিত্তিক সতর্কতা যুক্তিসঙ্গত, তবে সঠিক রূপে: “disk ৮০%” নয়, বরং “এই গতিতে ৪ ঘণ্টায় disk ভরবে” (একটি পূর্বাভাসভিত্তিক নিয়ম)। আর ৮০% অবস্থা দিনের বেলার ticket-এ যেতে পারে।
TLS certificate-এর মেয়াদও একই: শেষ হলে সবার কাছে সাইট ভাঙা দেখাবে, আর আগে নবায়ন সহজ। তাই “১৪ দিনের মধ্যে মেয়াদ শেষ” একটি ভালো ticket, আর “২ দিনের মধ্যে” page হতে পারে।
কারণ-ভিত্তিক মাপের আসল জায়গা dashboard. Alert বাজার পরে on-call প্রথম যেখানে তাকাবেন, সেখানে pool-এর ব্যস্ততা, event loop lag, gateway অনুযায়ী ব্যর্থতা পাশাপাশি থাকলে কারণ খুঁজে পেতে সময় কমে।
মূল উপলব্ধি: মানুষকে ডাকুন ক্রেতার ক্ষতিতে, অথবা এমন কারণে যা নিশ্চিত ক্ষতির দিকে যাচ্ছে আর আগে ঠেকানো যায়; বাকি কারণ খোঁজার জন্য সাজিয়ে রাখুন।
প্র ০৩Error tracker-এ ব্যবহারকারীর তথ্য পাঠালে ডিবাগ সহজ হয়। গোপনীয়তার সাথে এই সুবিধার ভারসাম্য কীভাবে করবেন?
প্রথমে বুঝুন, error tracker-এ পাঠানো মানে ডেটা একটি তৃতীয় পক্ষের কাছে যাওয়া, প্রায়ই অন্য দেশের server-এ। ইউরোপের GDPR (২০১৮ থেকে কার্যকর)-এর মতো আইনে এমন সেবাকে সাধারণত data processor ধরা হয়, আর তার সাথে চুক্তি, উদ্দেশ্য আর সংরক্ষণের সময় স্পষ্ট রাখতে হয়। বাংলাদেশেও ব্যক্তিগত তথ্য সুরক্ষা নিয়ে আইনি কাঠামো তৈরি হচ্ছে; আপনার ব্যবসার জন্য কোনটি প্রযোজ্য, আইনজ্ঞের সাথে মিলিয়ে নিন। আইন না থাকলেও নীতিটা একই: ক্রেতা আপনাকে বিশ্বাস করে ঠিকানা দিয়েছেন, অন্য কোম্পানির dashboard-এ দেখানোর জন্য নয়।
প্রকৌশলের দিক থেকে মূল প্রশ্ন: ডিবাগের জন্য আসলে কী লাগে? প্রায় সবসময় একটি স্থির অভ্যন্তরীণ id যথেষ্ট, যেমন u_8421. এটি দিয়ে কতজন আক্রান্ত গোনা যায়, আর দরকার হলে আপনার নিজের database-এ নিরাপদে মানুষটিকে খোঁজা যায়। ফোন নম্বর বা email লাগে না। আমাদের Sentry run-এ sendDefaultPii: false রেখে শুধু id পাঠানো হয়েছিল।
দ্বিতীয় প্রশ্ন: কতদিন রাখবেন? ৯০ দিনের পুরোনো event খুব কম কাজে লাগে। সংরক্ষণের সময় ছোট রাখলে ফাঁসের ক্ষতিও ছোট।
তৃতীয়: কে দেখতে পারেন? Error tracker-এ পুরো দলের প্রবেশ থাকে, প্রায়ই চুক্তিভিত্তিক freelancer-সহ। Database-এর মতো সেখানে কড়া নিয়ন্ত্রণ সাধারণত থাকে না।
ঝুঁকির সবচেয়ে বড় জায়গা session replay আর breadcrumb. Breadcrumb-এ URL থাকে, আর URL-এ reset token বা search-এর লেখা থাকতে পারে। Replay-তে form-এর লেখা। তাই সেখানে default-এ সব mask করে, দরকারমতো খোলা নিরাপদ পথ।
মূল উপলব্ধি: ডিবাগের জন্য মানুষকে চেনার দরকার নেই, ঘটনাকে চেনার দরকার। স্থির id, ছোট সংরক্ষণকাল আর পাঠানোর আগে মোছা, এই তিনে প্রায় সব সুবিধা থাকে, ঝুঁকি থাকে না।
প্র ০৪Hosted monitoring টুল কিনবেন, না open-source টুল নিজে চালাবেন? একটি ছোট দলের জন্য কোনটি ঠিক?
এখানে community-তে কোনো একক উত্তর নেই, আর দুই পক্ষেরই শক্ত যুক্তি আছে।
Hosted-এর পক্ষে: monitoring-এর সবচেয়ে বড় শর্ত হলো এটি আপনার অ্যাপের সাথে একসাথে পড়বে না। নিজের server-এ Prometheus আর error tracker চালালে, server পড়লে monitoring-ও পড়ে, ঠিক যখন দরকার। তাছাড়া ছোট দলের প্রতিটি ঘণ্টা দামি; Prometheus, Grafana, Loki, trace backend, alert routing, প্রত্যেকটির update, backup, disk-এর হিসাব একটি আলাদা কাজ। Hosted সেবা এই ভার নেয়।
Self-hosted-এর পক্ষে: খরচ ব্যবহারের সাথে বাড়ে; event, log আর metric series-এর সংখ্যা বাড়লে hosted সেবার বিল অনেক সময় অপ্রত্যাশিত হয়। ডেটা নিজের নিয়ন্ত্রণে থাকে, যা গোপনীয়তা বা আইনের কারণে জরুরি হতে পারে। আর শেখার দিক থেকে নিজে চালানো অনেক কিছু শেখায়।
এই বিতর্কের সবচেয়ে বড় পরিবর্তন এনেছে OpenTelemetry. ২০১৯ সালে OpenTracing আর OpenCensus মিলে এটি তৈরি হয়, লক্ষ্য ছিল instrumentation-কে vendor থেকে আলাদা করা। আপনার কোড OTel-এ instrument করা থাকলে, exporter বদলে hosted থেকে self-hosted বা এক vendor থেকে আরেকটিতে যাওয়া তুলনামূলক সহজ। তাই সিদ্ধান্তটি আর চিরকালের নয়।
ছোট দলের জন্য একটি বাস্তব পথ: error tracking আর বাইরের uptime check দিয়ে শুরু, কারণ এ দুটি সবচেয়ে কম কাজে সবচেয়ে বেশি দেয়। Metric আর trace OTel বা prom-client দিয়ে instrument করুন, যাতে পরে যেকোনো দিকে যাওয়া যায়। আর uptime checker অবশ্যই আপনার hosting-এর বাইরে রাখুন। কোন সেবার কী শর্ত, সেটি তাদের বর্তমান docs আর মূল্যতালিকা থেকে নিজে যাচাই করুন; এই পর্ব কোনো নির্দিষ্ট vendor সুপারিশ করে না।
মূল উপলব্ধি: আগে ঠিক করুন কী মাপবেন আর কীভাবে instrument করবেন, মান অনুযায়ী (OTel, Prometheus format); কোথায় পাঠাবেন, সেটি পরে বদলানো যায় এমন সিদ্ধান্ত রাখুন।
প্র ০৫সব error ধরা আর সব trace রাখা কি সম্ভব? Sampling করলে কি গুরুত্বপূর্ণ ঘটনা হারাবে না?
ছোট সাইটে সব রাখা সম্ভব, বড় হলে নয়। দিনে ৪২ লাখ request মানে দিনে ৪২ লাখ trace, প্রতিটিতে ৫-১০টি span. সেগুলো পাঠাতে network, রাখতে storage, আর অ্যাপের ভেতরে প্রতিটি span-এর CPU খরচ আছে। তাই sampling, অর্থাৎ একটি অংশ রাখা।
Google-এর Dapper গবেষণাপত্র (২০১০) দেখিয়েছিল, খুব কম অনুপাতে sample করেও বড় সিস্টেমের আচরণ বোঝা যায়, কারণ সাধারণ request-গুলো একই রকম। সমস্যা হলো বিরল ঘটনা: ধীরতম ০.১% request বা একটি বিশেষ ভুল এলোমেলো sampling-এ হারিয়ে যেতে পারে।
এর দুটি প্রচলিত সমাধান। Head sampling-এ request-এর শুরুতেই সিদ্ধান্ত হয়, আর traceparent-এর শেষের flag (-01 মানে sampled) বাকি service-কে জানায়। সস্তা, কিন্তু শুরুতে কেউ জানে না request-টি ব্যর্থ হবে কি না। Tail sampling-এ পুরো trace শেষ হওয়ার পরে collector সিদ্ধান্ত নেয়: ভুল থাকলে বা ধীর হলে রাখা হয়, বাকিগুলোর একটি অংশ মাত্র। দামি, কারণ সব span কিছুক্ষণ ধরে রাখতে হয়, কিন্তু গুরুত্বপূর্ণ ঘটনা হারায় না। বিস্তারিত আছে Tracing পর্বে।
Error-এর ক্ষেত্রে নিয়ম ভিন্ন: সাধারণত সব error পাঠানো হয়, কারণ grouping-ই আয়তন সামলায়, আর প্রতিটি নতুন issue জানা দরকার। তবে একটি ভুল যদি এক মিনিটে লাখবার ঘটে (একটি loop-এর ভেতরে), তখন SDK বা server-এর rate limit বাঁচায়। আর metric কখনো sample হয় না, কারণ সেটি আগে থেকেই সংক্ষিপ্ত সংখ্যা; তাই error rate-এর alert metric থেকে নিন, sample করা trace থেকে নয়।
মূল উপলব্ধি: গোনার কাজ metric-এর, গল্প বলার কাজ trace আর error-এর। গোনা হবে পূর্ণ, গল্প রাখা হবে বেছে, আর বাছাইয়ের নিয়ম এমন হবে যাতে ভুল আর ধীর ঘটনা সবার আগে টিকে থাকে।
অনুশীলন
হিসাব, কমান্ড, সমালোচনা আর আউটপুট অনুমান, চারটিই আছে।
-
Error budget (হিসাব): একটি API-র SLO ৩০ দিনে ৯৯.৫%. মাসে ধরে নেওয়া ১২ লাখ request, প্রতিদিন সমান। (ক) সময়ের হিসাবে budget কত মিনিট? (খ) কতটি ব্যর্থ request অনুমোদিত? (গ) হঠাৎ error হার ৬% হলো এবং থেকে গেল। Burn rate কত, আর পুরো budget কত দিনে শেষ হবে?
(ক) 30 × 24 × 60 × 0.005 = ২১৬ মিনিট, অর্থাৎ ৩ ঘণ্টা ৩৬ মিনিট। (খ) 12,00,000 × 0.005 = ৬,০০০টি। (গ) Burn rate = 0.06 ÷ 0.005 = ১২. Budget শেষ হবে 30 ÷ 12 = ২.৫ দিনে। মিলিয়ে দেখুন: দিনে 40,000 request, তার ৬% = 2,400টি ব্যর্থ; 6,000 ÷ 2,400 = ২.৫ দিন।
-
নিজের হাতে (কমান্ড): পর্ব ৩১-এর মতো একটি Express অ্যাপ লোকালে চালান। এই পর্বের
uptime-check.mjs-এ একটি তৃতীয় check যোগ করুন:/metrics, প্রত্যাশা 200, timeout 1000 ms. তারপর (ক) স্বাভাবিক অবস্থায় (খ) অ্যাপ বন্ধ করে চালান, আর প্রতিবারecho $?দেখুন। প্রতিটির আউটপুট কেমন হবে, আর এই script-কে প্রতি মিনিটে চালাতে Linux-এ cron-এর লাইন কী?(ক) তিনটি লাইনই
UP, প্রতিটিতেHTTP 200 in … ms, আর$?0. (খ) তিনটিইDOWN … ECONNREFUSED after … ms,$?1. Cron-এর লাইন, যেমন:* * * * * node /opt/checks/uptime-check.mjs https://example.com || /opt/checks/notify.sh. তবে মনে রাখুন, একই server-এ চালালে server পড়লে check-ও পড়বে; এটি অন্য একটি machine বা বাইরের সেবা থেকে চালান, আর পরপর দুবার ব্যর্থতায় খবর দিন। -
নকশা সমালোচনা: একজন সহকর্মী বাজারঘরের জন্য এই alert তালিকা প্রস্তাব করেছেন, সবগুলো রাত-দিন page করবে: (১) CPU > 80% ১ মিনিট (২) যেকোনো একটি 500 error (৩) প্রতিটি নতুন JavaScript issue (৪) Checkout-এর ৫ মিনিটের error হার > 1% (৫)
/healthzএকবার ব্যর্থ (৬) Disk > 70%. কোনগুলো রাখবেন, কোনগুলো বদলাবেন, আর কী যোগ করবেন?- (১) কারণ, লক্ষণ নয়; dashboard-এ রাখুন, page নয়।
- (২) একটি 500 মানে কিছু নয়; budget-ভিত্তিক burn rate alert দিয়ে বদলান।
- (৩) প্রতিটি নতুন issue-তে page অসহনীয়; দিনের সারাংশ বা ticket, শুধু অনেক ব্যবহারকারী আক্রান্ত হলে page.
- (৪) লক্ষণ, ভালো দিক, কিন্তু একা ৫ মিনিটের জানালা ঝাঁকুনিতে বাজবে; লম্বা জানালার সাথে মিলিয়ে multi-window burn rate করুন।
- (৫) একবারের ব্যর্থতায় ঝাঁকুনি; একাধিক জায়গা থেকে পরপর ২ বার ব্যর্থ হলে page.
- (৬) ৭০% স্থির সীমা কাজের নয়; “এই গতিতে কয়েক ঘণ্টায় ভরবে” ধরনের পূর্বাভাসে page, ৭০%-এ ticket.
- যোগ করুন: ব্যবসার সংকেত (যেমন স্বাভাবিক সময়ে ১ ঘণ্টা কোনো সফল order নেই), TLS মেয়াদের সতর্কতা, আর প্রতিটি page-এর runbook.
-
আউটপুট অনুমান: এই পর্বের fingerprint ফাংশন (বার্তার সংখ্যা
<n>, ফোন<phone>, frame-এর ফাইল নাম/*.js, লাইন-column বাদ) দিয়ে নিচের চারটি event-কে কয়টি issue-তে ভাগ করা হবে? (ক)Error: cart 12 empty, frameat checkout (app/x1.js:1:50)(খ)Error: cart 98 empty, frameat checkout (app/z9.js:1:77)(গ)Error: cart 12 empty, frameat payNow (app/x1.js:1:50)(ঘ)TypeError: cart 12 empty, frameat checkout (app/x1.js:1:50)৩টি issue. (ক) আর (খ) এক: বার্তা দুটিই
cart <n> empty, frame দুটিইcheckout app/*.js. (গ) আলাদা, কারণ ফাংশনের নামpayNow. (ঘ) আলাদা, কারণ ধরনTypeError, যা চাবির প্রথম অংশ। সুতরাং issue-গুলো: {ক, খ}, {গ}, {ঘ}.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমProduction-এ একটি browser ভুল থেকে একটি issue পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Event handler-এ TypeError ছোড়া হলো
window-এরerrorlistener বার্তা, ফাইল, লাইন আর column নিল- Receiver আকার যাচাই করে চেনা field রাখল আর ব্যক্তিগত তথ্য মুছল
- Source map minified অবস্থানকে মূল ফাইলের লাইনে অনুবাদ করল
- Fingerprint মিলিয়ে event একটি issue-তে জমা হলো
Next.js 16 App Router-এ একটি Server Component render-এর সময় ভুল ছুড়লে server-side error reporting-এর জন্য Next.js কোন hook দেয়?
pages/_error.js-এরgetInitialPropserror.tsx-এরuseEffectinstrumentation.ts-এরonRequestErrorwindow.addEventListener('error')
Production-এ একটি Next.js Server Component-এর ভুল error.tsx-এ পৌঁছালে browser-এর কোডে আসল ভুলটি server-এর log-এর সাথে কীভাবে মেলানো যায়?
error.messageথেকে আসল বার্তা পড়েerror.digestদিয়ে, যা server-এর report-এও একই থাকেerror.stack-এর প্রথম লাইন দিয়ে- ভুলের সময় মিলিয়ে log-এ খুঁজে
একটি সেবার SLO ৩০ দিনে ৯৯.৯%. গত এক ঘণ্টায় request-এর ১.৫% ব্যর্থ হয়েছে। এই এক ঘণ্টার burn rate কত?
- ১.৫
- ০.১৫
- ৯৯.৯
- ১৫
একটি SPA-র production build-এ browser-এর ভুলের stack trace দেখাচ্ছে at r (app.min.js:1:81). এটিকে applyCoupon (cart.js:6:26)-এ ফেরাতে কী লাগে?
- ওই build-এর source map ফাইল
- শুধু release tag
- Breadcrumb-এর তালিকা
- একই কোড আবার build করে নতুন ফাইল থেকে মিলিয়ে
বাজারঘরের সব server-side metric সবুজ: latency কম, 5xx শূন্য। তবু এক ঘণ্টা কোনো সফল order আসেনি, অথচ সাধারণত শতাধিক আসে। কোন ধরনের monitoring এটি সবচেয়ে ভালো ধরবে?
- CPU ব্যবহারের alert
/healthz-এর uptime check- সফল order-এর মতো ব্যবসার সংকেত
- Server log-এ error খোঁজা
একটি HTTP request-এ traceparent: 00-ae19b26289627244de439ff283018ba8-9a2a043c054a23f0-01 header আছে। 9a2a043c054a23f0 অংশটি কী?
- পুরো trace-এর id
- পাঠানো span-এর id, যা গ্রহণকারীর span-এর parent হবে
- Sampling flag
- ব্যবহারকারীর id
একটি error tracker-এ event পাঠানোর আগে ব্যক্তিগত তথ্য সামলানোর সবচেয়ে ভালো ক্রম কোনটি?
- নিজের কোডে allowlist, তারপর scrubber, তারপর পাঠানো
- সব পাঠানো, তারপর tracker-এর dashboard-এ মুছে দেওয়া
- পুরো request body পাঠানো, শুধু regex দিয়ে ফোন নম্বর ঢেকে
- ফোন নম্বর hash করে পাঠানো, বাকি সব অপরিবর্তিত
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- দেখুন চারটি golden signal আর ব্যবসার সংকেত, server আর browser দুই দিকেই; বাইরে থেকে uptime check রাখুন।
- Error tracker ভুল পাঠায় breadcrumb, release, environment-সহ; source map আসল লাইন দেয়, fingerprint এক ধরনের ভুল এক issue-তে রাখে।
- Next.js 16-এ
onRequestError(server),error.tsx/global-error.tsx(render,digestদিয়ে জোড়া), আরwindow-এরerror/unhandledrejection(browser). - ব্যক্তিগত তথ্য পাঠানোর আগে, নিজের কোডে সরান: আগে allowlist, তারপর scrubber.
- Alert হবে লক্ষণে: SLO, error budget আর দুই জানালার burn rate; প্রতিটি page-এর runbook, আর পালা করে on-call.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৮৪ · Web performance — Core Web Vitals, image আর cache headerপরবর্তী পর্বCore Web Vitals, ছবি আর cache header: পাতা দ্রুত করা, মেপে মেপে।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপরের পর্ব ৮৪, Web performance, সহ পুরো রোডম্যাপ।
- পর্ব ৩১ · Log আর configআগের ভিত্তিpino, request id, health check আর প্রাথমিক metric.
- Cloud & DevOps · Alerting আর SLOগভীরে যানBurn rate, Alertmanager আর on-call-এর নকশা।
- Cloud & DevOps · Distributed tracing আর OpenTelemetryপাশের ট্র্যাকCollector, sampling আর Jaeger-এ আসল trace.
- System Design · Observabilityপাশের ট্র্যাকবড় সিস্টেমে log, metric আর trace-এর নকশা।