এই পর্বে যা শিখবেন
- Container-এ কেন stdout-এ log লিখতে হয়, আর কোন level কখন
- Free text থেকে structured JSON, আর trace id দিয়ে তিনটি service-এর log জোড়া
- একটি আসল pipeline: Fluent Bit থেকে Loki, HTTP API দিয়ে LogQL query
- Elasticsearch-এর মতো সব index করা বনাম Loki-র মতো শুধু label index করা
- মাসিক log খরচ, sampling আর retention tier-এর হিসাব
- PII আর secret মুছে ফেলা, audit log-এর আলাদা নিয়ম, আর হারানো log-এর হিসাব
১ · ssh আর grep কেন কয়েকটি server পেরোতে পারে না
রাত ২টায় একটি কাল্পনিক অনলাইন বাজার অ্যাপে অর্ডার আটকে যাচ্ছে। পর্ব ৯-এ আপনি শিখেছেন grep, awk আর pipe দিয়ে log পড়া। একটি server হলে সেটিই যথেষ্ট। পাঁচটি server হলেও একটি loop চলে। কিন্তু সংখ্যা বাড়লে কী হয়?
# ssh into every server and grep. Fine for 3 servers, painful for 50.
for host in web-1 web-2 web-3; do
ssh "$host" "grep 'order 502' /var/log/app/app.log"
doneএই loop চারটি জায়গায় ভেঙে পড়ে।
Server মুছে যায়
পর্ব ১৭-এর autoscaling রাত ৩টায় যে instance সরিয়ে দিল, তার log-ও সাথে গেল। Container মুছলে একই কাণ্ড ঘটে।
একটি request, অনেক server
একটি অর্ডার গেছে orders, payment আর notify দিয়ে। আলাদা server-এর আলাদা ফাইল হাতে জোড়া লাগাতে হয়।
Access ছড়িয়ে পড়ে
Log দেখতে হলে সবাইকে production-এ ssh দিতে হয়। এতে ঝুঁকি বাড়ে, আর কে কী পড়ল তার হিসাবও থাকে না।
হিসাব করা যায় না
“গত এক ঘণ্টায় কোন service-এ কতটি error?” প্রশ্নের উত্তর দিতে পঞ্চাশটি ফাইল পড়া লাগে, আর log rotation-এ পুরোনো ফাইল ততক্ষণে সরে গেছে।
একটি বড় কুরিয়ার কোম্পানির কথা ভাবুন। প্রতিটি শাখায় নিজের খাতা। পার্সেল হারালে আপনি একে একে ৫০টি শাখায় ফোন করছেন। আবার কোনো শাখা বন্ধ হলে তার খাতাও গায়েব। সমাধান হলো প্রতিটি ঘটনা সাথে সাথে একটি কেন্দ্রীয় হিসাবঘরে পাঠানো, যেখানে পার্সেলের নম্বর দিয়ে এক জায়গায় পুরো যাত্রা দেখা যায়। Centralized logging ঠিক সেই হিসাবঘর, আর পার্সেল নম্বরটি হলো trace id.
Log হলো সময়ের ক্রমে ঘটে যাওয়া ঘটনার রেকর্ড। Centralized logging মানে সেই রেকর্ড যেখানে তৈরি হয় সেখান থেকে সরিয়ে আনা, যাতে server মরে গেলেও প্রমাণ বেঁচে থাকে। তখন log আর ফাইল নয়, খোঁজা যায় এমন একটি ডেটা।
২ · Container-এ log: stdout আর log level
আগে app নিজে /var/log/app.log ফাইলে লিখত, নিজেই rotation করত। Container-এ এই পদ্ধতি অসুবিধার। Container-এর writable layer তার সাথেই মুছে যায় (পর্ব ৩৯). তাই নিয়ম বদলেছে।
The Twelve-Factor App-এর একাদশ factor বলে: log হলো ঘটনার একটি স্রোত। App নিজের log কোথায় যাবে তা নিয়ে ভাববে না। সে শুধু stdout-এ লিখবে, বাকিটা চারপাশের ব্যবস্থার দায়িত্ব। Heroku-র প্রকৌশলীদের এই নির্দেশিকা ২০১১ সালের দিকে প্রকাশিত হয়।
এরপর কী ঘটে, তা নির্ভর করে runtime-এর ওপর। Docker-এ logging driver সেটি নেয়। অনেক ইনস্টলেশনে default হলো json-file, আর সেটি নিজে থেকে rotation করে না, তাই max-size না দিলে disk ভরে যেতে পারে। Kubernetes-এ node-এর /var/log/pods-এর নিচে ফাইল জমে, kubelet rotation করে, আর একটি DaemonSet agent সেই ফাইল পড়ে পাঠায়।
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}Log level: কোন খবরের কত ওজন
Log level হলো একটি line-এর গুরুত্বের দাগ। হাসপাতালের জরুরি বিভাগের মতো ভাবুন: সবাই রোগী, কিন্তু কার আগে কাকে দেখতে হবে তার ক্রম আছে। Syslog-এর RFC 5424 (২০০৯) আটটি severity ঠিক করে দিয়েছে। App-এ সাধারণত চার-পাঁচটিই লাগে।
| Level | কখন | উদাহরণ | Production-এ |
|---|---|---|---|
debug | ডেভেলপারের খুঁটিনাটি | cart loaded | সাধারণত বন্ধ |
info | স্বাভাবিক ব্যবসায়িক ঘটনা | order received | চালু, প্রয়োজনে sampling |
warn | অস্বাভাবিক, কিন্তু কাজ চলছে | sms queue slow | রাখুন |
error | একটি কাজ ব্যর্থ হয়েছে | gateway timeout | সব রাখুন |
fatal / critical | Process চলতে পারছে না | db config missing | সব রাখুন, alert |
একটি প্রত্যাশিত ঘটনাকে error লিখবেন না। ভুল পাসওয়ার্ড দেওয়া ব্যবহারকারীর ভুল, সিস্টেমের নয়। Error-এর সংখ্যা যদি প্রতিদিন হাজারে হাজারে আসে, কেউ আর সেটি দেখে না, আর আসল error ভিড়ে হারিয়ে যায়।
৩ · Free text থেকে structured log, আর trace id
নিচের দুটি line একই ঘটনা বলছে। প্রথমটি মানুষ পড়ে। দ্বিতীয়টি মানুষ আর যন্ত্র দুজনেই পড়ে।
2026-10-30 10:00:02 ERROR payment t-1002 gateway timeout after 3000ms
{"ts":"2026-10-30T10:00:02","level":"error","service":"payment",
"trace_id":"t-1002","msg":"gateway timeout","ms":3000}
প্রথম line থেকে 3000 বের করতে হলে একটি নির্দিষ্ট লেখার ধরনের ওপর ভরসা করে regex লিখতে হয়। কেউ বাক্যটি একটু বদলালেই সেটি নীরবে ভাঙে। Structured log-এ নাম ধরে জিজ্ঞেস করা যায়: ms > 2000. বিয়েবাড়ির অতিথি তালিকার মতো: এলোমেলো খাতার চেয়ে নাম, ফোন আর আসন নম্বরের ঘর কাটা খাতায় খোঁজা অনেক সহজ।
নিচের ছোট Python app-টি একই code দিয়ে তিনটি কাল্পনিক service হিসেবে চলে। লক্ষ করুন, এটি stdout-এ লেখে, আর প্রতিটি line-এ একটি JSON object. এই ধরনকে অনেকে NDJSON বলেন। কার্ডের নম্বরটি আপনার পরীক্ষার জন্য ইচ্ছা করে রাখা ভুল, আর সবগুলোই নকল।
import json, logging, os, sys
SERVICE = os.environ["SERVICE"]
class JsonFormatter(logging.Formatter):
def format(self, r):
event = {"ts": self.formatTime(r, "%Y-%m-%dT%H:%M:%S"),
"level": r.levelname.lower(), "service": SERVICE,
"msg": r.getMessage()}
event.update(getattr(r, "fields", {}))
return json.dumps(event)
h = logging.StreamHandler(sys.stdout) # stdout, never a file
h.setFormatter(JsonFormatter())
log = logging.getLogger(SERVICE)
log.addHandler(h)
log.setLevel(os.environ.get("LOG_LEVEL", "INFO"))
# … six fake orders: (trace_id, order_id, amount, card)
for trace, order, amount, card in ORDERS:
f = {"trace_id": trace, "order_id": order}
if SERVICE == "orders":
log.debug("cart loaded", extra={"fields": f})
log.info("order received", extra={"fields": {**f, "amount": amount}})
elif SERVICE == "payment":
log.info(f"charging card {card}", extra={"fields": f})
if trace in ("t-1002", "t-1005"):
log.error("gateway timeout", extra={"fields": {**f, "ms": 3000}})
# … notify logs "sms sent to rahim@example.com"এই পর্বের Docker আউটপুটের আগে একটি কথা: সব transcript Docker Engine 29 (BuildKit)-এ একটি আসল run থেকে নেওয়া, দীর্ঘ অংশ … দিয়ে ছাঁটা। Container id, সময়, size আর timing আপনার মেশিনে আলাদা হবে।
{"ts": "2026-10-30T21:07:17", "level": "info", "service": "payment", "msg": "charging card 4111 1111 1111 1111", "trace_id": "t-1002", "order_id": 502}
{"ts": "2026-10-30T21:07:17", "level": "error", "service": "payment", "msg": "gateway timeout", "trace_id": "t-1002", "order_id": 502, "ms": 3000}
লক্ষ করুন, debug line আসেনি, কারণ level INFO. এটি এক ধরনের খরচ নিয়ন্ত্রণ, আর LOG_LEVEL নামের environment variable দিয়েই বদলানো যায়। পুরোনো free text কিন্তু রাতারাতি বদলাবে না। সেগুলোর জন্য agent বা নিচের মতো script-কে parse করতে হয়।
#!/usr/bin/env bash
set -euo pipefail
cat > app.log <<'EOF'
2026-10-30 10:00:01 INFO orders trace=t-1001 order received amount=450
2026-10-30 10:00:01 INFO payment trace=t-1001 card charged ms=210
2026-10-30 10:00:02 ERROR payment trace=t-1002 gateway timeout ms=3000
Traceback (most recent call last):
File "pay.py", line 42, in charge
2026-10-30 10:00:03 ERROR payment trace=t-1002 retry failed ms=3000
2026-10-30 10:00:03 WARN notify trace=t-1003 sms queue slow ms=900
2026-10-30 10:00:04 ERROR notify trace=t-1003 sms provider down ms=120
2026-10-30 10:00:05 ERROR payment trace=t-1002 order cancelled ms=15
EOF
# 1. Free text -> named fields. Lines that do not fit go to a separate file.
awk '$1 ~ /^20[0-9][0-9]-/ && $5 ~ /^trace=/ {
trace = substr($5, 7)
msg = $6; for (i = 7; i <= NF; i++) msg = msg " " $i
printf "ts=%sT%s level=%s service=%s trace_id=%s msg=\"%s\"\n",
$1, $2, tolower($3), $4, trace, msg > "fields.log"
next
}
{ print > "unparsed.log" }' app.log
echo "--- first 2 structured lines"
head -2 fields.log
echo "--- lines that did not fit: $(wc -l < unparsed.log)"
# 2. Count errors by trace id: a question free text answers badly.
echo "--- errors by trace id"
grep 'level=error' fields.log | grep -o 'trace_id=[^ ]*' | sort | uniq -c | sort -rn
# 3. One trace, every service it touched, in time order.
echo "--- trace t-1002"
grep 'trace_id=t-1002' fields.log | sed 's/ trace_id=[^ ]*//'Script-টি আগে নামহীন লেখাকে ঘরে ঘরে ভাগ করে। আউটপুটের মূল অংশ:
--- lines that did not fit: 2
--- errors by trace id
3 trace_id=t-1002
1 trace_id=t-1003
দুটি line ধরা পড়েনি, আর সেগুলো Python-এর stack trace. একটি error কয়েক line জুড়ে ছড়িয়ে গেলে line-ভিত্তিক parser ভাঙে। Agent-এ এর জন্য আলাদা multiline নিয়ম লাগে, আর JSON log-এ পুরো stack trace একটি field-এ ঢোকে, তাই সমস্যাটিই থাকে না। গণনায় দেখুন, t-1002-এর তিনটি error আসলে একটিই ব্যর্থ request. শুধু error গুনলে মনে হতো তিনটি আলাদা সমস্যা।
Trace id: টুকরো log জোড়ার সুতা
একটি request যখন কয়েকটি service পার হয়, প্রতিটি service আলাদা log লেখে। সেগুলো জোড়ার উপায় হলো শুরুতে একটি অনন্য id বানিয়ে পুরো পথে সাথে নিয়ে যাওয়া। অনেকে একে correlation id বলেন, আর distributed tracing-এর ভাষায় trace id. W3C Trace Context (২০২০-এর Recommendation) এর একটি মানক HTTP header ঠিক করেছে, traceparent. Trace-এর পূর্ণ গল্প পর্ব ৬৫ (Distributed tracing আর OpenTelemetry)-এ আসবে। এখানে দরকার শুধু একটি নিয়ম: id ছাড়া log জোড়া যায় না।
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
ver │ trace id (16 byte) │ parent span │ flags
- orders
t-1002id তৈরি - payment
t-1002header-এ পেল - notify
t-1002header-এ পেল - Log store
trace_id=t-1002এক query-তে সব
{"level":"info","service":"orders","trace_id":"t-1002","msg":"order received"}
POST /charge
X-Trace-Id: t-1002
{"level":"info","service":"payment","trace_id":"t-1002","msg":"charging card"}
{"level":"error","service":"payment","trace_id":"t-1002","msg":"gateway timeout","ms":3000}
{"level":"info","service":"notify","trace_id":"t-1002","msg":"payment failed sms sent"}
orders info order received
payment info charging card
payment error gateway timeout ms=3000
notify info payment failed sms sent
- শুরু। ব্যবহারকারীর অর্ডার orders service-এ এলো। আগত request-এ কোনো id নেই, তাই সে
t-1002বানাল আর নিজের line-এ রাখল। - Id চলল পথে। payment-কে ডাকার সময় orders id-টি header-এ পাঠাল। payment সেটি পড়ে নিজের প্রতিটি line-এ রাখছে, নতুন id বানাচ্ছে না। এখানেই বেশিরভাগ ভুল হয়: একটি service header না পাঠালে সুতা ছিঁড়ে যায়।
- ব্যর্থতা। কাল্পনিক gateway ৩ সেকেন্ডে সাড়া দিল না। payment একটি
errorলিখল, সাথেms=3000. - তৃতীয় service. orders ব্যর্থতা জেনে notify-কে ডাকল, একই id পাঠিয়ে। notify অন্য server-এ, অন্য দলের হলেও তার line-এ একই
t-1002আছে। - জোড়া। Central store-এ একটি query-তে চারটি line পাশাপাশি। রাত ২টায় পঞ্চাশটি ফাইল না খুলে আপনি ৩ সেকেন্ডের timeout দেখতে পেলেন। ব্যবহারকারী বা order id field থাকলে সাপোর্ট দলও অভিযোগ থেকে এই id-তে পৌঁছাতে পারে।
৪ · Log pipeline: agent থেকে query পর্যন্ত
পণ্য যেমন কারখানা থেকে সরাসরি আপনার বাসায় আসে না, log-ও আসে ধাপে ধাপে। Log pipeline-এর পাঁচটি কাজ আছে। বাস্তবে একটি tool একাধিক কাজ করতে পারে, কিন্তু কাজগুলো আলাদা করে চিনলে সমস্যা খোঁজা সহজ হয়।
Log-এর পথ: লঞ্চঘাট থেকে গুদাম
Application → agent → buffer → aggregator → store → query
| ধাপ | কাজ | লঞ্চঘাটের উপমা |
|---|---|---|
| Agent (shipper) | Log পড়ে metadata জুড়ে পাঠায় | ঘাটের কুলি মাল তোলে, ট্যাগ লাগায় |
| Buffer | গতির ফারাক সামলায় | ঘাটের গুদাম, লঞ্চ দেরি করলে মাল জমে |
| Aggregator | কেন্দ্রীয়ভাবে parse, redact, sampling | সদরঘাটের বাছাই কেন্দ্র |
| Store | জমা রাখে, index করে | মূল গুদাম আর রেজিস্টার |
| Query | খোঁজা, গণনা, alert | রেজিস্টার দেখে মাল খুঁজে বের করা |
ছোট ব্যবস্থায় agent সরাসরি store-এ পাঠাতে পারে। আপনার পরের run-এও তাই হবে। বড় ব্যবস্থায় মাঝে একটি aggregator আর queue বসে, যাতে নিয়ম এক জায়গায় থাকে। Kubernetes-এ agent সাধারণত DaemonSet হিসেবে চলে, কম ক্ষেত্রে Pod-এর sidecar হিসেবে।
এবার একটি আসল line-কে ধাপে ধাপে অনুসরণ করুন। প্রতিটি ধাপে তার চেহারা বদলাচ্ছে। লাইনগুলো পরের অংশের run থেকে নেওয়া।
- Appstdout
- Agentগ্রহণ
- Filterparse, redact
- Bufferchunk
- Storelabel + line
- QueryLogQL
{"ts": "…", "level": "info", "service": "payment",
"msg": "charging card 4111 1111 1111 1111",
"trace_id": "t-1002", "order_id": 502}
tag: shop.payment
log: "{\"ts\": \"…\", \"level\": \"info\", …}"
container_name: /abcl-lg-payment
source: stdout
level: info
service: payment
trace_id: t-1002
msg: charging card [CARD-1111]
chunk: 1-1793394430.577747292.flb flush: প্রতি 1 সেকেন্ডে Store বন্ধ? chunk থেকে যায়, পরে retry
{job="shop", level="info", service="payment"}
{"trace_id":"t-1002","order_id":502,
"ts":"2026-10-30T21:09:43",
"msg":"charging card [CARD-1111]"}
{job="shop"} | json | trace_id="t-1002"
- জন্ম। App একটি JSON line stdout-এ লিখল। কাল্পনিক ভুল হিসেবে এতে পুরো নকল কার্ড নম্বর আছে। এই অবস্থায় বেরিয়ে গেলে সেটি store, backup আর সবার চোখে পৌঁছাবে।
- Agent-এর হাতে। Docker-এর driver line-টি পাঠাল, সাথে container-এর নাম আর উৎস। Agent-এর কাছে JSON-টি এখনো একটি সাধারণ string, নাম
log. - Filter.
parserstring-টিকে আসল field বানাল।luafilter কার্ডের শেষ চারটি অঙ্ক ছাড়া সব মুছল।Reserve_Dataচালু না থাকায় এই run-এ container-এর নামের field-ও বাদ গেছে। এই ছোট setup-এ agent নিজেই aggregator-এর কাজ করছে, তাই filter buffer-এর আগে বসেছে। - Buffer. Line একটি chunk-এ জমল, যা প্রতি ১ সেকেন্ডে পাঠানো হয়। এই run-এ buffer শুধু memory-তে, তাই agent মরলে না-পাঠানো chunk-ও মরবে।
- Store.
levelআরserviceline থেকে সরে label হলো। একই label-এর line মিলে একটি stream. Loki শুধু এই label index করে, line-টি সংকুচিত chunk-এ রাখে। - Query. LogQL আগে label দিয়ে stream বাছে, তারপর সেই stream-এর line খুলে JSON পড়ে,
trace_idমেলায়। এ জন্য ফল ৩টি হলেও পড়তে হয়েছে ১৮টি line.
৫ · আসল run: Fluent Bit থেকে Loki, তারপর LogQL
এবার পুরো পথটি চালানো যাক। Fluent Bit হলো C-তে লেখা ছোট agent. Grafana Loki হলো log store. Docker-এর fluentd driver container-এর stdout সরাসরি Fluent Bit-এ পাঠায়। সব কিছু নিজের ল্যাপটপে, তাই কোনো cloud খরচ নেই। Image দুটির size এই run-এ ১৬৫ MB আর ১৫৭ MB. চালালে শেষের clean-up অংশটিও চালান।
service:
flush: 1
log_level: info
parsers_file: /fluent-bit/etc/parsers.conf
pipeline:
inputs:
- name: forward
listen: 0.0.0.0
port: 24224
mem_buf_limit: 8MB
filters:
- name: parser # the "log" string becomes real fields
match: '*'
key_name: log
parser: json
- name: lua # redaction, before anything leaves
match: '*'
script: /cfg/redact.lua
call: redact
outputs:
- name: loki
match: '*'
host: abcl-lg-loki
port: 3100
labels: job=shop
label_keys: $service,$level
line_format: json
retry_limit: 5function redact(tag, ts, record)
local m = record["msg"]
if m == nil then return 0, ts, record end
m = m:gsub("%d%d%d%d[ -]?%d%d%d%d[ -]?%d%d%d%d[ -]?(%d%d%d%d)", "[CARD-%1]")
m = m:gsub("[%w._-]+@[%w.-]+", "[EMAIL]")
record["msg"] = m
return 2, ts, record
end$ docker network create abcl-lg-net
$ docker run -d --name abcl-lg-loki --hostname demo --network abcl-lg-net \
-p 127.0.0.1:18641:3100 grafana/loki:3.5.5 \
-config.file=/etc/loki/local-config.yaml
$ docker run -d --name abcl-lg-fluent --hostname demo --network abcl-lg-net \
-p 127.0.0.1:18642:24224 -v "$PWD:/cfg:ro" \
fluent/fluent-bit:4.0 -c /cfg/fluent-bit.yaml
$ docker logs abcl-lg-fluent 2>&1 | tail -3
… [ info] [input:forward:forward.0] storage_strategy='memory' (memory only)
… [ info] [input:forward:forward.0] listening on 0.0.0.0:24224
… [ info] [output:loki:loki.0] configured, hostname=abcl-lg-loki:3100
$ for s in orders payment notify; do
docker run --name abcl-lg-$s --hostname demo --network abcl-lg-net \
-e SERVICE=$s -v "$PWD:/app:ro" \
--log-driver fluentd \
--log-opt fluentd-address=127.0.0.1:18642 \
--log-opt tag=shop.$s \
python:3.12-alpine python /app/app.py >/dev/null
doneএবার LogQL, Loki-র query ভাষা। HTTP API-তে curl দিয়ে জিজ্ঞেস করা হলো। JSON লম্বা বলে নিচে শুধু দরকারি অংশ রাখা হয়েছে।
$ B=http://127.0.0.1:18641/loki/api/v1
$ curl -s $B/labels
{"status":"success","data":["job","level","service","service_name"]}
# every line of one trace, oldest first
$ curl -sG $B/query_range --data-urlencode 'direction=forward' \
--data-urlencode 'query={job="shop"} | json | trace_id="t-1002"'
orders info {"trace_id":"t-1002","order_id":502,"ts":"2026-10-30T21:09:42","amount":1200,"msg":"order received"}
payment error {"trace_id":"t-1002","order_id":502,"ts":"2026-10-30T21:09:43","ms":3000,"msg":"gateway timeout"}
payment info {"trace_id":"t-1002","order_id":502,"ts":"2026-10-30T21:09:43","msg":"charging card [CARD-1111]"}
# a metric from logs: errors per service in the last hour
$ curl -sG $B/query --data-urlencode \
'query=sum by (service) (count_over_time({job="shop",level="error"}[1h]))'
{"status":"success","data":{"resultType":"vector","result":[{"metric":{"service":"payment"},"value":[…,"2"]}],…
# clean up: exact names only
$ docker rm -f abcl-lg-orders abcl-lg-payment abcl-lg-notify abcl-lg-fluent abcl-lg-loki
$ docker network rm abcl-lg-net
$ docker rmi grafana/loki:3.5.5 fluent/fluent-bit:4.0তিনটি জিনিস লক্ষ করুন। এক, কার্ডের নম্বর store-এ নেই, redaction কাজ করেছে। দুই, app charging card আগে লিখেছিল, কিন্তু ফলে gateway timeout আগে এসেছে। এই run-এ timestamp সেকেন্ডে গোল হয়ে পৌঁছেছে, তাই একই সেকেন্ডের line-এর ক্রম আর নিশ্চিত নয়। তিন, service_name label আমরা দিইনি, Loki নিজে যোগ করেছে। Tool-এর এমন আচরণ version-ভেদে বদলায়, তাই নিজের চোখে দেখা দরকার।
Query-র উত্তরের ভেতরে stats অংশে Loki জানায় সে কত পড়েছে। এই সংখ্যাগুলোই পরের অংশের মূল কথা। সবগুলো এই run-এর।
| Query | পড়া line | পড়া byte | ফল |
|---|---|---|---|
{job="shop",level="error"} | 2 | 210 | 2 |
{job="shop"} | json | trace_id="t-1002" | 18 | 1879 | 3 |
{job="shop"} |= "1111 1111" | 18 | 1879 | 0 |
{job="shop"} |= "@example.com" | 18 | 1879 | 0 |
{job="shop"} |= "CARD-1111" | 18 | 1879 | 4 |
শেষ তিনটি সারি redaction-এর প্রমাণও: আসল নম্বর আর ই-মেইল শূন্যবার মিলেছে। মোট ১৮টি line চারটি label-এর দলে ভাগ: payment error ২, notify info ৪, orders info ৬, payment info ৬।
৬ · সব index করা বনাম শুধু label: কোনটি কখন
বইয়ের শেষে শব্দসূচি থাকলে যেকোনো শব্দ সাথে সাথে পাওয়া যায়, কিন্তু সেই সূচি বানাতে আর ছাপতে খরচ হয়। আবার শুধু অধ্যায়ের নাম থাকলে বই পাতলা ও সস্তা হয়, কিন্তু অধ্যায়টি পড়ে খুঁজতে হয়। Log store-এর মূল সিদ্ধান্ত এটিই।
পুরো শব্দসূচি: Elasticsearch
Elasticsearch (২০১০) আর তার fork OpenSearch (২০২১) Lucene-এর inverted index ব্যবহার করে। প্রতিটি শব্দ আর field index হয়। কঠিন খোঁজা খুব দ্রুত, কিন্তু লেখা ভারী, আর index-এর জন্য disk, memory ও CPU বেশি লাগে।
লেখা দামি, খোঁজা সস্তাঅধ্যায়ের নাম: Loki
Loki (Grafana Labs, ২০১৮) শুধু label index করে, লেখাগুলো সংকুচিত chunk-এ object storage-এ রাখে। লেখা আর জমা সস্তা। কিন্তু label ছাড়া খুঁজলে line খুলে পড়তে হয়, যেমন উপরের ১৮ বনাম ২।
লেখা সস্তা, খোঁজা ভারীLabel-এর ফাঁদ। Loki-তে label-এর প্রতিটি অনন্য মিলন একটি নতুন stream. service আর level মিলে এখানে ৪টি stream. trace_id-কে label করলে প্রতিটি request একটি stream হবে। একে বলে high cardinality. Index ফুলে যায়, chunk ছোট ছোট হয়, আর store ধীর হয়। নিয়ম: label হোক কম মানের জিনিস (service, level, env), আর trace id থাকুক line-এর ভেতরে। Loki 3.x-এ এর জন্য structured metadata নামে একটি মাঝামাঝি পথও আছে।
| প্রশ্ন | সব index করা | শুধু label |
|---|---|---|
| অজানা শব্দ খোঁজা | দ্রুত | ধীর, পরিমাণের ওপর নির্ভর |
| জমা রাখার খরচ | বেশি | কম, object storage-এ |
| নিরাপত্তা বিশ্লেষণ, পূর্ণ লেখা খোঁজা | স্বাভাবিক পছন্দ | সীমিত |
| চেনা service-এর debug | ভালো, তবে দামি | ভালো, label জানা থাকলে |
| চালানোর ঝামেলা | Cluster tuning, shard | কম, তবে label শৃঙ্খলা জরুরি |
Cloud-এর তৈরি সেবা
নিজে না চালাতে চাইলে প্রতিটি provider-এর log সেবা আছে। ধারণা একই, নাম আলাদা। নিচের তালিকা লেখার সময়ের, সুবিধা আর দাম বদলায়, তাই provider-এর ডকুমেন্টেশন দেখে নিশ্চিত হবেন।
| কাজ | AWS | Azure | Google Cloud |
|---|---|---|---|
| Log সেবা | CloudWatch Logs | Azure Monitor Logs | Cloud Logging |
| জমার পাত্র | Log group | Log Analytics workspace | Log bucket |
| Query | Logs Insights | KQL | Logging query language |
| Log থেকে metric | Metric filter | Log search alert | Log-based metric |
| বাইরে পাঠানো | Subscription filter | Diagnostic setting, export | Log Router sink |
| Audit log | CloudTrail | Activity log | Cloud Audit Logs |
Managed সেবায় চালানোর ঝামেলা নেই, তবে দাম সাধারণত GB ধরে, আর default retention সব জায়গায় এক নয়। কিছু সেবায় default হলো “চিরকাল রাখা”। নতুন log group তৈরির পর retention দেখে নেওয়া একটি ভালো অভ্যাস।
৭ · খরচ: sampling, retention আর log-based metric
Log-এর বিল বিদ্যুৎ বিলের মতো: একবার ব্যবহারের অভ্যাস তৈরি হলে মাস শেষে হঠাৎ চোখে পড়ে। হিসাবের সূত্র সরল।
ধরি ২০০ request/s, আর প্রতি request-এ ৪টি ৩৫০ byte-এর info line. তাহলে 200 × 4 × 350 × 2,592,000 ÷ 10⁹ = ৭২৫.৮ GB. ৩০ দিনের মাসে ২,৫৯২,০০০ সেকেন্ড ধরা হয়েছে। নিচের model-এ চারটি level আর চারটি পরিকল্পনা আছে। সব দাম কল্পিত গোল সংখ্যা। আসল দামের জন্য provider-এর pricing পাতা দেখবেন। সংখ্যা বদলে আবার চালিয়ে দেখুন।
// Monthly log volume and cost. Prices are ASSUMED round numbers, not any provider's.
const RPS = 200, SECONDS = 30 * 86400; // requests per second, 30-day month
const PER_GB = 0.50, STORE_GB_MONTH = 0.03; // ingest price, stored price
const HOT_DAYS = 30, COMPRESS = 5; // keep 30 days; stored size = raw / 5
const LEVELS = { // lines per request, bytes per line
debug: { lines: 12, bytes: 300 },
info: { lines: 4, bytes: 350 },
warn: { lines: 0.1, bytes: 400 },
error: { lines: 0.02, bytes: 900 },
};
function plan(name, keep) { // keep = share kept per level, 0 to 1
let total = 0;
const parts = [];
for (const [lvl, v] of Object.entries(LEVELS)) {
const gb = RPS * v.lines * v.bytes * SECONDS / 1e9 * keep[lvl];
total += gb;
parts.push(`${lvl} ${gb.toFixed(1)}`);
}
const ingest = total * PER_GB;
const stored = total / COMPRESS * (HOT_DAYS / 30);
const store = stored * STORE_GB_MONTH;
console.log(`${name}\n GB/month: ${parts.join(", ")} = ${total.toFixed(1)}`);
console.log(` ingest $${ingest.toFixed(2)} + store $${store.toFixed(2)}` +
` = $${(ingest + store).toFixed(2)} per month`);
}
plan("A. everything", { debug: 1, info: 1, warn: 1, error: 1 });
plan("B. debug off", { debug: 0, info: 1, warn: 1, error: 1 });
plan("C. debug off, info 10%", { debug: 0, info: 0.1, warn: 1, error: 1 });
plan("D. debug 1%, info 10%", { debug: 0.01, info: 0.1, warn: 1, error: 1 });পরিকল্পনাগুলোর আসল আউটপুট:
A. everything
GB/month: debug 1866.2, info 725.8, warn 20.7, error 9.3 = 2622.1
ingest $1311.03 + store $15.73 = $1326.77 per month
B. debug off
GB/month: debug 0.0, info 725.8, warn 20.7, error 9.3 = 755.8
ingest $377.91 + store $4.53 = $382.45 per month
C. debug off, info 10%
GB/month: debug 0.0, info 72.6, warn 20.7, error 9.3 = 102.6
ingest $51.32 + store $0.62 = $51.94 per month
D. debug 1%, info 10%
GB/month: debug 18.7, info 72.6, warn 20.7, error 9.3 = 121.3
ingest $60.65 + store $0.73 = $61.38 per month
A থেকে C-তে বিল প্রায় ২৫ ভাগের এক ভাগ, অথচ warn আর error-এর একটি line-ও হারায়নি। এই model-এ ingest-ই বিলের প্রায় সবটা, তাই জমার মেয়াদ কমানোর চেয়ে ঢোকার আগে ছাঁটাই বেশি কাজের। দামের কাঠামো provider-ভেদে আলাদা, কাজেই নিজের সংখ্যা বসিয়ে দেখবেন।
Sampling: বুদ্ধি করে কিছু ফেলে দেওয়া
Sampling মানে সব line না রেখে একটি অংশ রাখা। ঈদের আগে বাসের টিকিটের ভিড় মাপতে প্রতিটি যাত্রী গুনতে হয় না, প্রতি দশজনে একজন গুনলেই ধারণা মেলে। তিনটি নিয়ম মনে রাখুন।
- Error কখনো sampling নয়। বিরল আর দামি খবরই সবার আগে রাখুন।
- Line ধরে নয়, trace ধরে।
hash(trace_id)দিয়ে সিদ্ধান্ত নিলে একটি request-এর সব line থাকে, অথবা সব যায়। এলোমেলো line রাখলে যাত্রার গল্পে ফাঁক পড়ে। - গুণক লিখে রাখুন। ১০% রাখলে line-এ
sample_rate=10রাখুন, যাতে গণনার সময় ১০ দিয়ে গুণ করা যায়।
Debug প্রয়োজনমতো কিছুক্ষণের জন্য একটি service-এ চালু করার ব্যবস্থাও রাখা যায়, যেমন feature flag দিয়ে (পর্ব ৫৯). শেষে আবার বন্ধ করতে ভুলবেন না।
Retention tier: নতুন log দামি ঘরে, পুরোনো গুদামে
নতুন log প্রতিদিন খোঁজা হয়, পুরোনোটি প্রায় কখনো নয়। তাই retention-কে স্তরে ভাগ করা হয়: দ্রুত hot, মাঝারি warm, সস্তা archive. পর্ব ২১-এর lifecycle rule এই কাজটিই করে। নিচে ১০০ GB-র একটি দিনের log-কে অনুসরণ করুন। কল্পিত দাম: hot $০.৩০, warm $০.০৫, archive $০.০০৪, প্রতি GB-মাসে।
- Hot
0–7 দিন$0.30 - Warm
8–30 দিন$0.05 - Archive
31–365 দিন$0.004 - মুছে ফেলা
365 দিন$0
| দিন | |
| কোথায় | |
| খোঁজার সময় | |
| এ পর্যন্ত খরচ |
- দিন ০. আজকের ১০০ GB hot tier-এ এলো। এখানে SSD আর index আছে, তাই খোঁজা সেকেন্ডে। এটি সবচেয়ে দামি ঘর।
- দিন ৭. ৭ দিন hot-এ: 100 × 7/30 × 0.30 = $৭.০০. Incident-এর বেশিরভাগ খোঁজাখুঁজি এই সময়ের মধ্যেই শেষ, তাই log warm-এ নামল।
- দিন ৩০. ২৩ দিন warm-এ: 100 × 23/30 × 0.05 = $৩.৮৩. মোট $১০.৮৩। এবার log archive-এ গেল, object storage-এর সস্তা শ্রেণিতে।
- দিন ২০০. ১৭০ দিন archive-এ: 100 × 170/30 × 0.004 = $২.২৭. মোট $১৩.১০। একজন নিরীক্ষক পুরোনো log চাইলে আগে restore করতে হয়, এতে সময় আর আলাদা খরচ লাগতে পারে।
- দিন ৩৬৫. বাকি ১৬৫ দিন: 100 × 165/30 × 0.004 = $২.২০. মোট $১৫.৩০। নীতিমতো log মুছে গেল। পুরো বছর hot-এ রাখলে একই ডেটার খরচ হতো 100 × 365/30 × 0.30 = $৩৬৫.
প্রতিদিন ১০০ GB এলে স্থির অবস্থায় hot-এ থাকবে ৭০০ GB, warm-এ ২,৩০০ GB, archive-এ ৩৩,৫০০ GB. মাসিক খরচ: 700 × 0.30 + 2,300 × 0.05 + 33,500 × 0.004 = 210 + 115 + 134 = $৪৫৯. সবটা hot-এ রাখলে 36,500 × 0.30 = $১০,৯৫০. ফারাক প্রায় ২৪ গুণ। মেয়াদ কত হবে, সেটি শুধু প্রকৌশলের সিদ্ধান্ত নয়: আইন, চুক্তি আর নিরীক্ষার চাহিদা দেখে ঠিক করতে হয়।
Log-based metric: গণনা রেখে line ছাঁটাই
Log-based metric মানে log থেকে একটি সংখ্যা বের করে metric হিসেবে রাখা। উপরের count_over_time query-টি ঠিক তাই। এতে সুবিধা দুটি: Sampling-এর আগে গণনা করলে সঠিক সংখ্যা পাওয়া যায়, আর metric জমানো log জমানোর চেয়ে সস্তা। তবে এটি একটি অস্থায়ী সেতুর মতো: যে সংখ্যা প্রতিদিন লাগে, সেটি সরাসরি app থেকে metric হিসেবে বের করাই ভালো (পর্ব ৬৩, Metric আর Prometheus). Alert-এর নকশা পর্ব ৬৬ (Alerting আর SLO)-তে আসবে।
৮ · PII, secret আর audit log
Log হলো সেই জায়গা, যেখানে সবাই সবকিছু ফেলে দেয়। তাই এটি সহজেই সবচেয়ে কম পাহারার সংবেদনশীল ভাণ্ডার হয়ে ওঠে। PII হলো এমন তথ্য যা দিয়ে একজন মানুষকে চেনা যায়: নাম, ফোন, ই-মেইল, NID. এর সাথে secret: password, OTP, token, API key (পর্ব ৩৩).
২০১৮ সালে Twitter প্রকাশ্যে জানায়, একটি bug-এর কারণে কিছু password hash হওয়ার আগেই ভেতরের log-এ লেখা হয়ে গিয়েছিল, আর ব্যবহারকারীদের password বদলাতে বলা হয়। এমন ভুলের জন্য কোনো হ্যাকার লাগে না, একটি log.info(request)-ই যথেষ্ট। কার্ড-সংক্রান্ত PCI DSS, ইউরোপের GDPR আর বিভিন্ন দেশের তথ্য-সুরক্ষা নিয়মও এখানে প্রাসঙ্গিক। বাংলাদেশে কোন নিয়ম এখন প্রযোজ্য, তা হালনাগাদ আইনি সূত্রে যাচাই করবেন।
Redaction মানে সংবেদনশীল অংশ মুছে বা ঢেকে দেওয়া। আগের run-এ Lua filter সেটিই করেছে। নিচের filter-এ সব ডেটা নকল। একটি নিয়ম বদলে বা একটি নতুন line যোগ করে দেখুন।
// A redaction filter with FAKE data. Change RULES or LINES and press Run again.
const RULES = [
["card", /\b(?:\d{4}[ -]?){3}(\d{4})\b/g, "[CARD-$1]"],
["email", /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g, "[EMAIL]"],
["phone", /\b01[3-9]\d{8}\b/g, "[PHONE]"],
["bearer", /Bearer\s+[\w.-]+/g, "Bearer [TOKEN]"],
["secret", /\b(password|otp|api_key)=\S+/gi, "$1=[SECRET]"],
];
const LINES = [
"login ok user=rahim@example.com ip=203.0.113.7",
"charge card=4111 1111 1111 1111 amount=450",
"sms to 01700000000 otp=482913",
"GET /me Authorization: Bearer abc.def.ghi",
"db connect password=FakePass123 host=db-1",
"kyc nid 1990123456789 name=Rahim", // no rule for this one!
];
const hits = {};
function redact(line) {
for (const [name, re, sub] of RULES)
line = line.replace(re, (...m) => {
hits[name] = (hits[name] || 0) + 1;
return sub.replace("$1", m[1]);
});
return line;
}
for (const line of LINES) console.log(redact(line));
console.log("hits:", JSON.stringify(hits));
// A denylist misses what nobody thought of. An allowlist keeps only known fields.
const ALLOW = new Set(["ts", "level", "service", "msg", "trace_id", "order_id"]);
const event = { ts: "10:00:01", level: "info", service: "kyc", msg: "verified",
trace_id: "t-1", nid: "1990123456789", name: "Rahim" };
const kept = Object.entries(event).filter(([k]) => ALLOW.has(k));
const dropped = Object.keys(event).filter(k => !ALLOW.has(k));
console.log(JSON.stringify(Object.fromEntries(kept)));
console.log("dropped fields:", dropped.join(", "));login ok user=[EMAIL] ip=203.0.113.7
charge card=[CARD-1111] amount=450
sms to [PHONE] otp=[SECRET]
GET /me Authorization: Bearer [TOKEN]
db connect password=[SECRET] host=db-1
kyc nid 1990123456789 name=Rahim
hits: {"email":1,"card":1,"phone":1,"secret":2,"bearer":1}
{"ts":"10:00:01","level":"info","service":"kyc","msg":"verified","trace_id":"t-1"}
dropped fields: nid, name
শেষের kyc line-টি দেখুন: NID আর নাম অক্ষত বেরিয়ে গেছে, কারণ কেউ তার নিয়ম লেখেনি। এটিই denylist-এর দুর্বলতা। Allowlist উল্টো কাজ করে: জানা field ছাড়া সব বাদ, তাই নতুন field নিজে থেকেই আটকে যায়। IP ঠিকানাও অনেক আইনে ব্যক্তিগত তথ্য ধরা হয়, এখানে সেটি রয়ে গেছে। ভেবে দেখুন, আপনার ক্ষেত্রে রাখা উচিত কি না।
১. Code-এ allowlist: পুরো request বা object log না করা। ২. Library-তে নির্দিষ্ট field মুছে ফেলা। ৩. Pipeline-এ regex filter, শেষ জাল হিসেবে। ৪. Store-এ কম মানুষের access, encryption আর ছোট retention. Pipeline-কে একমাত্র ভরসা ভাববেন না। আর সত্যিই secret ফাঁস হলে log মোছা যথেষ্ট নয়, secret-টি বদলাতে হবে।
Audit log আলাদা জিনিস
Application log ডেভেলপারের কাজের খাতা। Audit log হলো ব্যাংকের লেনদেনের খাতা: কে, কখন, কোথা থেকে, কী করলেন (পর্ব ৩৪). নিয়মও আলাদা।
| বিষয় | Application log | Audit log |
|---|---|---|
| পাঠক | ডেভেলপার, SRE | নিরাপত্তা দল, নিরীক্ষক |
| Sampling | চলে | না, প্রতিটি ঘটনা চাই |
| হারানো | কিছুটা সহনীয় | গ্রহণযোগ্য নয় |
| বদলানো | Tier বদলাতে পারে | Immutable, আলাদা account |
| গন্তব্য | Log store | আলাদা archive, SIEM |
৯ · Backpressure আর হারানো log
একটি ফানেলে আপনি যত দ্রুত পানি ঢালছেন, নিচ দিয়ে তত দ্রুত না গেলে পানি উপচে পড়বে। Backpressure হলো নিচের ধীরগতির চাপ ওপরে পৌঁছানো। Log pipeline-এ এমন অবস্থায় বিকল্প মাত্র তিনটি।
জমিয়ে রাখা
Memory বা disk buffer-এ রাখা। সাময়িক সমস্যা পার হওয়া যায়, তবে জায়গা সীমিত।
ফেলে দেওয়া
Line হারাবে, কিন্তু app চলবে। Application log-এর জন্য সাধারণত এটিই বেছে নেওয়া হয়।
আটকে দেওয়া
App-কে থামানো। কিছুই হারায় না, কিন্তু log-এর জন্য আসল request ধীর হয়।
Docker-এর fluentd driver এর ভালো উদাহরণ। Default blocking mode-এ driver ধীর হলে app-এর stdout write আটকে যেতে পারে। --log-opt mode=non-blocking দিলে একটি ring buffer বসে, আর সেটি ভরলে line হারায়। আবার Fluent Bit-এ mem_buf_limit ছাড়ালে input থেমে যায়। সিদ্ধান্তটি আপনাকে সচেতনভাবে নিতে হবে, default-এ ছেড়ে দিলে চলবে না।
আগের pipeline-এই পরীক্ষা করা হলো: Loki বন্ধ করে আবার orders চালানো হলো, যা ৬টি line লেখে।
$ docker stop abcl-lg-loki
$ docker run --name abcl-lg-orders2 … python:3.12-alpine python /app/app.py >/dev/null
$ docker logs abcl-lg-fluent 2>&1 | tail -3
… [ warn] [net] getaddrinfo(host='abcl-lg-loki', err=4): Domain name not found
… [error] [output:loki:loki.0] no upstream connections available
… [ warn] [engine] failed to flush chunk '1-1793394430.577747292.flb', retry in 12 seconds: task_id=0, input=forward.0 > output=loki.0 (out_id=0)
$ docker start abcl-lg-loki
$ docker logs abcl-lg-fluent 2>&1 | tail -1
… [ info] [engine] flush chunk '1-1793394430.577747292.flb' succeeded at retry 2: task_id=0, input=forward.0 > output=loki.0 (out_id=0)
# orders lines in Loki: 6 before, now 12. Nothing was lost this time.এবার কিছুই হারায়নি, কারণ বন্ধ ছিল কিছুক্ষণ, আর agent নিজে বেঁচে ছিল। Retry-র মাঝের সময় exponential backoff-এ বাড়ে। কিন্তু এই buffer শুধু memory-তে ছিল, আর retry_limit: 5 শেষ হলে chunk ফেলে দেওয়া হতো। এবার দীর্ঘ outage-এর হিসাব করা যাক।
// Buffer and backpressure on a simulated clock. Store is down from t=60 to t=240 s.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const END = 600, OUT_RATE = 1500; // seconds simulated; lines/s the store accepts
function run(name, buf, mode) { // mode: "drop" or "block"
const rnd = mulberry32(42);
let q = 0, sent = 0, lost = 0, born = 0, slowed = 0, peak = 0, late = 0;
for (let t = 0; t < END; t++) {
const made = Math.round(800 + rnd() * 400); // 800 to 1200 lines this second
const up = t < 60 || t >= 240;
const room = buf - q;
if (made <= room) { q += made; born += made; }
else if (mode === "drop") { q = buf; born += made; lost += made - room; }
else { q = buf; born += room; slowed++; late += made - room; } // app waits
if (up) { const out = Math.min(q, OUT_RATE); q -= out; sent += out; }
peak = Math.max(peak, q);
}
console.log(`${name}\n made ${born + late}, delivered ${sent}, in buffer ${q}, ` +
`lost ${lost}, peak ${peak}`);
if (mode === "block") console.log(` app stalled ${slowed} s, ${late} lines never written`);
}
run("A. small memory buffer, drop", 50000, "drop");
run("B. big disk buffer, drop", 400000, "drop");
run("C. small buffer, block the app", 50000, "block");A. small memory buffer, drop
made 601142, delivered 469710, in buffer 0, lost 131432, peak 50000
B. big disk buffer, drop
made 601142, delivered 599088, in buffer 2054, lost 0, peak 180462
C. small buffer, block the app
made 601142, delivered 469710, in buffer 0, lost 0, peak 50000
app stalled 132 s, 131432 lines never written
৩ মিনিটের outage-এ গড়ে ১,০০০ line/s হারে প্রায় ১,৮০,০০০ line জমে। ৫০,০০০-এর buffer-এ বাকিটা হারায় (A), নয়তো app ১৩২ সেকেন্ড আটকে থাকে (C). B-তে কিছুই হারায়নি, তবে শেষে এখনো ২,০৫৪ line বাকি, কারণ backlog কমে মাত্র (1500 − 1000) = ৫০০ line/s হারে। সূত্র দুটি মনে রাখুন: দরকারি buffer = আগমনের হার × outage-এর সময়, আর ফাঁকা হওয়ার সময় = backlog ÷ (বেরোনোর হার − আগমনের হার). এখানে 180,000 ÷ 500 = ৩৬০ সেকেন্ড। একটি line ৩০০ byte হলে ১,৮০,০০০ line প্রায় ৫৪ MB.
একটি নীরব ফাঁদ: যখন সবচেয়ে বেশি log তৈরি হয়, ঠিক তখনই pipeline-এ সবচেয়ে বেশি চাপ পড়ে। Incident-এর সময়েই log হারানোর সম্ভাবনা সবচেয়ে বেশি। তাই agent-এর নিজের metric দেখুন: drop, retry আর buffer-এর ব্যবহার। Pipeline নিজেই একটি production service.
আরও গভীরে: বেশিরভাগ log pipeline at-least-once পাঠায়: retry-তে একই line দুবার যেতে পারে, তাই store-এ duplicate সম্ভব। Kafka-র মতো টেকসই queue মাঝে বসালে store দীর্ঘক্ষণ বন্ধ থাকলেও line জমে থাকে, আর একই স্রোত থেকে একাধিক গন্তব্যে পাঠানো যায়, তবে একটি নতুন cluster চালানোর দায়ও আসে। Loki-র নকশা Prometheus-এর label মডেল থেকে অনুপ্রাণিত, আর log-এর জন্য OpenTelemetry একটি vendor-নিরপেক্ষ data model ও Collector দিচ্ছে। Promtail-এর জায়গায় Grafana এখন Alloy-র দিকে যেতে বলছে, তাই পুরোনো টিউটোরিয়াল দেখে নতুন কিছু শুরু করার আগে সেটি যাচাই করবেন।
পরীক্ষা ও ইন্টারভিউতে:
- Container-এ log ফাইলে নয়, stdout-এ কেন? Container মুছলে ফাইল যায়, আর platform stdout সংগ্রহ করতে পারে।
- Trace id কেন label নয়? প্রতিটি মান নতুন stream বানায়, এটি high cardinality.
- Elasticsearch বনাম Loki? সব index করলে খোঁজা দ্রুত, জমা দামি। Label index করলে জমা সস্তা, খোঁজা ভারী।
- Log buffer ভরে গেলে কী হয়? ফেলে দেওয়া, আটকে দেওয়া বা disk-এ রাখা, এই তিনের একটি।
- Audit log কেন sampling করা যায় না? প্রতিটি ঘটনা প্রমাণ, একটি হারালেও তদন্ত অসম্পূর্ণ।
১০ · AI-এর যুগে logging
AI খুব দ্রুত logging config আর query লিখে দেয়, আর সেগুলো দেখতে ঠিকঠাক লাগে। কিন্তু AI আপনার বিল দেখে না, আইন জানে না, আর log-এর ভেতরে কী আছে তাও দেখে না। Cloud আর DevOps-এর পরিচিত ভুলগুলোর সাথে এখানে log-এর নিজস্ব কিছু ভুল যোগ হয়।
| AI-এর ভুল | ফল | যাচাই |
|---|---|---|
trace_id বা user_id-কে label বানায় | High cardinality, store ধীর | Label-এর সম্ভাব্য মানের সংখ্যা গুনুন |
log.info(request) লেখে | Token আর PII log-এ | নকল secret পাঠিয়ে store-এ খুঁজুন |
Production-এ debug রেখে দেয় | বিল কয়েক গুণ | আগে মাসিক GB হিসাব করুন |
Store-কে 0.0.0.0-এ খুলে দেয়, auth ছাড়া | সব log সবার জন্য খোলা | Private network আর auth নিশ্চিত করুন |
Shipper-কে * permission দেয় | Least privilege ভাঙে | শুধু লেখার অনুমতি দিন |
latest tag আর নেই এমন flag দেয় | Config চলে না, বা নীরবে অন্য কিছু করে | নির্দিষ্ট version আর অফিসিয়াল ডকুমেন্টেশন |
Index মোছা বা rm -rf সতর্কতা ছাড়া | প্রমাণ হারায় | আগে throwaway পরিবেশে চালান, diff দেখুন |
AI-কে কাজ দেওয়ার সময় সীমাগুলো আগেই বলে দিন, আর নিজের আসল log পেস্ট করবেন না। নকল line ব্যবহার করুন।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Twelve-Factor বলে app log-এর গন্তব্য নিয়ে ভাববে না। এই নীতি কি সব সময় ঠিক?
এই নীতি একটি বড় ভাগাভাগি করে: app ঘটনা বলবে, platform সেটি সামলাবে। ২০১১ সালের দিকে Heroku-র প্রকৌশলীরা যখন এটি লেখেন, তখন তাঁদের চোখের সামনে ছিল হাজারো ক্ষণস্থায়ী process, যেখানে স্থানীয় ডিস্কের ওপর ভরসা করা যায় না। Docker (২০১৩) আর Kubernetes (২০১৪) এই ধারণাকে মূলধারায় নিয়ে আসে। লাভ হলো, একই image স্থানীয় ল্যাপটপে terminal-এ log দেখায়, আর production-এ agent সেটি নেয়। App-এর code-এ Loki বা CloudWatch-এর কোনো নাম নেই।
তবে এর সীমা আছে। প্রথমত, stdout একটি pipe. Pipe ভরলে লেখা আটকে যেতে পারে, অর্থাৎ “আমি শুধু লিখি” কথাটি পুরো সত্য নয়। Python-এর print pipe-এ লিখলে buffer করে, তাই crash-এর ঠিক আগের line হারাতে পারে; এ জন্যই unbuffered চালানো বা logging library-র flush দরকার। দ্বিতীয়ত, stdout-এ দুটি ভিন্ন ধরনের স্রোত মিশে যায়। Audit log, লেনদেনের রেকর্ড বা টাকা-সংক্রান্ত ঘটনা যদি হারানো চলে না, তাহলে সেগুলো ডেটাবেসে একই transaction-এ লেখা বা নির্ভরযোগ্য queue-তে পাঠানো ভালো। সেখানে app-কে জানতেই হয় গন্তব্য কোথায়, আর লেখা নিশ্চিত হলো কি না।
তৃতীয়ত, একটি পুরোনো সফটওয়্যার হয়তো শুধু ফাইলেই লেখে। তখন একটি sidecar সেই ফাইল পড়ে stdout-এ দিতে পারে। অর্থাৎ এখানে নীতি ভাঙা হচ্ছে না, পুরোনো software-কে নীতির কাছে আনা হচ্ছে।
মূল উপলব্ধি: Twelve-Factor সাধারণ পর্যবেক্ষণের log-এর জন্য চমৎকার default. কিন্তু যে ডেটা হারানো চলে না, সেটি log নয়, ব্যবসার ডেটা। তাকে সেই সম্মান দিয়েই রাখতে হয়।
প্র ০২Elasticsearch থাকতে Loki-র মতো “কম index” করা একটি ব্যবস্থার দরকার পড়ল কেন?
Doug Cutting-এর Lucene (১৯৯৯) পূর্ণ-পাঠ্য খোঁজার ভিত্তি গড়ে। Shay Banon ২০১০ সালে তার ওপর Elasticsearch আনেন, আর Logstash ও Kibana মিলে “ELK” log জগতের প্রায় সাধারণ পছন্দ হয়ে ওঠে। মূল ধারণাটি ছিল শক্তিশালী: প্রতিটি শব্দ index করলে যা খুশি খোঁজা যায়। সমস্যা শুরু হয় পরিমাণ বাড়লে। Log-এর বৈশিষ্ট্য হলো, লেখা হয় অনেক, পড়া হয় খুব কম। বেশিরভাগ line কেউ কখনো পড়ে না। অথচ প্রতিটি line-এর জন্য index-এর খরচ দেওয়া হচ্ছে, লেখার সময় CPU, পরে disk আর memory.
Grafana Labs ২০১৮ সালে Loki ঘোষণা করে “Prometheus-এর মতো, কিন্তু log-এর জন্য” এই ধারণা নিয়ে। প্রশ্নটি ছিল: বেশিরভাগ debug-এ আপনি তো আগেই জানেন কোন service, কোন সময়। তাহলে শুধু সেই label index করে বাকিটা সস্তা object storage-এ রাখলে কেমন হয়? এতে metric আর log একই label ধরে জোড়া যায়। দাম হলো, অজানা শব্দ খুঁজতে অনেক ডেটা খুলে পড়তে হয়। Loki এখন বড় পড়ার কাজ অনেক worker-এ ভাগ করে, আর bloom filter-এর মতো কৌশলও আনছে, অর্থাৎ দুই নকশা একে অন্যের দিকে একটু করে এগোচ্ছে।
ব্যবসা আর লাইসেন্সও ইতিহাসের অংশ। ২০২১ সালে Elastic, Apache 2.0 ছেড়ে SSPL আর Elastic License-এ গেলে AWS-এর নেতৃত্বে OpenSearch fork হয়। ২০২৪ সালে Elastic আগের দুটি রেখে AGPLv3-কেও বিকল্প হিসেবে ঘোষণা করে। Loki-ও ২০২১ সালে Apache 2.0 থেকে AGPLv3-এ গেছে। লাইসেন্স আরও বদলাতে পারে, তাই বেছে নেওয়ার সময় বর্তমান শর্ত দেখে নিতে হয়। ClickHouse-এর মতো কলামভিত্তিক ডেটাবেসও এখন log-এ ব্যবহৃত হচ্ছে, যা তৃতীয় একটি পথ।
মূল উপলব্ধি: ভালো বনাম মন্দ নয়, প্রশ্ন হলো কাজের ধরন। নিরাপত্তা তদন্তে অজানা শব্দ খুঁজলে পূর্ণ index দাম ফেরত দেয়। চেনা service-এর দৈনন্দিন debug-এ সস্তা জমাই বেশি কাজের। খরচ কোথায় দেবেন, লেখার সময়ে না পড়ার সময়ে, সিদ্ধান্ত সেটিই।
প্র ০৩Secret log-এ ঢোকে কেন, এত সতর্ক প্রতিষ্ঠানেও? Regex filter দিয়ে কি সমস্যা মেটে?
২০১৮ সালে Twitter আর GitHub দুই প্রতিষ্ঠানই আলাদাভাবে জানায়, একটি bug-এর কারণে কিছু password সাধারণ লেখায় ভেতরের log-এ চলে গিয়েছিল। ২০১৯ সালে Facebook-ও জানায়, তাদের ভেতরের সিস্টেমে বহু password সাধারণ লেখায় জমা ছিল। এই প্রতিষ্ঠানগুলোর নিরাপত্তা দল দুর্বল নয়। তাহলে এমন হয় কেন?
কারণটি গঠনগত। Log লেখা সহজ, আর ডিবাগের চাপে মানুষ পুরো object ছাপায়: log.debug(user), log.info(request). কেউ হয়তো ছয় মাস পরে সেই object-এ একটি নতুন field যোগ করে, আর log-এ কোনো বদল ছাড়াই secret ঢুকে পড়ে। Exception-এর message-এ connection string চলে আসে। URL-এর query string-এ token থাকে, আর access log সেটি লিখে রাখে। Crash dump-এ memory-র টুকরো থাকে।
Regex filter দরকারি, কিন্তু সীমিত। এটি শুধু চেনা ধরন ধরে: উপরের NID উদাহরণে যেমন দেখলেন। Luhn পরীক্ষা ছাড়া কার্ডের regex সাধারণ ১৬ অঙ্কের order id-কেও ধরতে পারে, আর ভুল মিলও ক্ষতি করে। প্রতি line-এ অনেক regex চালালে agent-এর CPU খরচও বাড়ে। তাই স্তর লাগে: টাইপ দিয়ে secret মোড়ানো যাতে তা ছাপাতে গেলে [hidden] দেখায়, field allowlist, secret scanning, store-এ কম access. একটি বিশেষ ধারণা হলো canary token: একটি নকল secret পাঠিয়ে নিয়মিত খুঁজে দেখা, সেটি কোথায় পৌঁছায়।
Logging library নিজেও ঝুঁকি হতে পারে। ২০২১ সালের ডিসেম্বরে Log4j-এর Log4Shell (CVE-2021-44228) দেখায়, log-এ ঢোকা অবিশ্বস্ত লেখাও বিপজ্জনক হতে পারে। প্রতিরক্ষা হলো নির্ভরতা হালনাগাদ রাখা, আর ব্যবহারকারীর দেওয়া লেখাকে সব সময় অবিশ্বস্ত ডেটা হিসেবে দেখা।
মূল উপলব্ধি: Log একটি ডেটাবেস, শুধু তাকে কেউ ডেটাবেসের মর্যাদা দেয় না। একই access control, একই সতর্কতা, একই retention-এর ভাবনা তার প্রাপ্য।
প্র ০৪Sampling করলে যে একটি বিরল bug-এর log হারিয়ে যেতে পারে। তাহলে কি sampling করা ঠিক?
ঝুঁকিটি আসল। ধরুন একটি bug ১০,০০০ request-এ একবার ঘটে, আর আপনি info-র ১% রাখছেন। তার আগের কারণ-বলা info line-গুলো সাধারণত থাকবে না। তবে বিকল্প কী? সব রাখা মানে কয়েক গুণ বেশি বিল। আসল প্রশ্ন তাই “sampling করব কি না” নয়, “কোন সময়ে, কোন তথ্য দেখে সিদ্ধান্ত নেব”।
Google-এর Dapper paper (২০১০) tracing-এ খুব কম হারে sampling-এর কথা বলে, আর দেখায় বড় স্কেলে সাধারণ সমস্যা তবুও ধরা পড়ে। সেখানে সিদ্ধান্ত হয় শুরুতে, একে বলে head sampling. এটি সস্তা, কিন্তু request শুরুতে কেউ জানে না সেটি ব্যর্থ হবে। Tail sampling সিদ্ধান্ত নেয় শেষে: পুরো request-এর line কিছুক্ষণ ধরে রেখে দেখে error বা ধীরগতি হলো কি না, হলে সবটা রাখে। ফল ভালো, কিন্তু মাঝের স্তরে memory লাগে, আর সব line এক জায়গায় আনতে হয়। এর পূর্ণ আলোচনা পর্ব ৬৫-তে আসবে।
আরেকটি পথ হলো কম line, বেশি তথ্য। Stripe তাদের প্রকৌশল ব্লগে “canonical log line”-এর কথা লিখেছে: প্রতি request-এর শেষে একটি মাত্র বড় line, যাতে সময়, ফল, ব্যবহারকারীর ধরন আর গুরুত্বপূর্ণ সব field থাকে। অনেকে একে wide event বলেন। ১০টি ছোট line-এর বদলে ১টি সমৃদ্ধ line রাখলে খরচ কমে, আর বিশ্লেষণ বাড়ে। ত্রুটির ক্ষেত্রেও একটি কৌশল আছে: একটি ring buffer-এ সাম্প্রতিক debug line রেখে দেওয়া, error হলে সেগুলোও পাঠানো।
মূল উপলব্ধি: Log ফেলে দেওয়া মানে তথ্য হারানো নয়, যদি আপনি জানেন কোনটি ফেলছেন। Error-এর সব, স্বাভাবিকের নমুনা, আর হিসাবের জন্য metric: এই তিন মিলিয়ে কম খরচে প্রায় পুরো ছবি পাওয়া যায়।
প্র ০৫কিছু log হারালে কি মেনে নেওয়া যায়? “Exactly once” log pipeline চাওয়া কি যুক্তিসঙ্গত?
Pipeline-এর দুটি সাধারণ আচরণ আছে। At-most-once: একবার পাঠিয়ে ভুলে যাওয়া, তাই duplicate নেই, তবে line হারাতে পারে। পুরোনো UDP syslog এমনই ছিল। At-least-once: নিশ্চয়তা না পাওয়া পর্যন্ত আবার পাঠানো। কিছু হারায় না (buffer থাকা পর্যন্ত), তবে একই line দুবার আসতে পারে। Retry-র সময় store হয়তো আগের chunk-টি নিয়েছিল, শুধু উত্তরটি পথে হারিয়েছে। এটি distributed system-এর পুরোনো সমস্যা: প্রেরক জানে না অপর পক্ষ পেল কি না।
“Exactly once” পেতে হলে প্রতিটি line-এ অনন্য id লাগবে, store-এ duplicate বাছা লাগবে, আর buffer টেকসই হতে হবে। এসব সম্ভব, কিন্তু প্রতিটি ধাপেই খরচ ও জটিলতা বাড়ে। সাধারণ application log-এ সেই দাম সাধারণত ফেরত আসে না। একটি debug line দুবার দেখা বা হাজারে একটি info হারানো বেশিরভাগ ক্ষেত্রে কোনো ক্ষতি করে না। উল্টোদিকে, log-এর জন্য app আটকে গেলে ব্যবহারকারীর আসল অর্ডারই ব্যর্থ হয়, যা অনেক বড় ক্ষতি। তাই বেশিরভাগ দল “ভালো চেষ্টা” নীতি নেয়, আর হারানোর হার মেপে রাখে।
Metric হিসেবে দুটি সংখ্যা দেখা ভালো। প্রথমটি হলো agent-এর drop count. দ্বিতীয়টি হলো একটি heartbeat: প্রতি মিনিটে একটি পরিচিত line পাঠিয়ে store-এ তা মিলল কি না দেখা। এতে “log নেই” মানে “সিস্টেম ঠিক আছে”, এমন ভুল ধারণা এড়ানো যায়। Log আসা বন্ধ হওয়াও একটি alert হওয়া উচিত।
ব্যতিক্রম হলো audit আর আর্থিক রেকর্ড। সেখানে হারানো গ্রহণযোগ্য নয়, আর সেগুলোর জন্য আলাদা, টেকসই পথ রাখা হয়, যেমন object storage-এ immutable লেখা। Cloud-এর audit সেবাগুলোও ফাইলের অখণ্ডতা যাচাইয়ের সুবিধা দেয়।
মূল উপলব্ধি: নির্ভরযোগ্যতার দাম আছে। প্রতিটি log-কে একই সুরক্ষা না দিয়ে আগে ঠিক করুন কোন ডেটা কতটা হারানো চলে। সেই সিদ্ধান্তই pipeline-এর নকশা ঠিক করে দেয়।
অনুশীলন
প্রথমে নিজে করুন, তারপর উত্তর মেলান।
-
মাসিক হিসাব (Numerical): একটি service ৫০০ request/s পায়, প্রতি request-এ ৫টি line, প্রতি line ৪০০ byte. ৩০ দিনের মাসে কত GB? ingest-এর কল্পিত দাম $০.৫০/GB হলে বিল কত? ৯০% বাদ দিলে কত?
প্রতি সেকেন্ডে 500 × 5 × 400 = 1,000,000 byte, অর্থাৎ ১ MB. প্রতিদিন 1 MB × 86,400 = ৮৬.৪ GB. মাসে 86.4 × 30 = ২,৫৯২ GB. বিল 2,592 × 0.50 = $১,২৯৬. ৯০% বাদ দিলে থাকে ২৫৯.২ GB, বিল $১২৯.৬০.
-
Label সমালোচনা (নকশা): একজন সহকর্মী Loki-তে এই label চান:
service,env,level,user_id,trace_id,status_code,pod. আপনি কোনগুলো রাখবেন, আর কেন?রাখুন:
service,env,level. এগুলোর মান কম আর স্থির। সরান:user_idআরtrace_id, কারণ প্রতিটি মান নতুন stream বানায়; এগুলো line-এ থাকুক। সতর্কভাবে:status_code-এর মান কম, তবে প্রতি service-এ stream গুণ হয়ে যায়, তাই line-এ রেখে| jsonদিয়ে ছেঁকে নেওয়া ভালো।podপ্রতিটি deploy-তে নতুন নাম পায়, তাই সময়ের সাথে stream জমে; দরকার হলে line-এ রাখুন। গুণটি মনে রাখুন: ৩টি service × ২টি env × ৪টি level = সর্বোচ্চ ২৪টি stream, অথচ ১ লাখ user মানে লাখের ঘরে। -
আউটপুট বলুন (Predict): নিচের command কী ছাপাবে?
printf 'ERROR a\nINFO b\nERROR b\nERROR b\nWARN a\nERROR c\n' \ | grep ERROR | sort | uniq -c | sort -rn2 ERROR b 1 ERROR c 1 ERROR agrepচারটিERRORline রাখে।sortএক রকম line পাশাপাশি আনে, কারণuniq -cশুধু পাশাপাশি থাকা line গোনে।sort -rnসংখ্যা ধরে বড় থেকে ছোটে সাজায়। সমান সংখ্যায় পুরো line উল্টো ক্রমে আসে, তাইcআগে। -
Outage-এর buffer (Numerical): Agent-এ ২,০০০ line/s আসে, store নিতে পারে ৫,০০০ line/s. Store ৫ মিনিট বন্ধ থাকলে কত line জমবে, ৩০০ byte ধরে কত MB, আর ফিরে আসার পর backlog শেষ হতে কত সময়?
Backlog = 2,000 × 300 = ৬,০০,০০০ line. আকার 600,000 × 300 = 180,000,000 byte = ১৮০ MB. ফাঁকা হওয়ার হার 5,000 − 2,000 = ৩,০০০ line/s, তাই সময় 600,000 ÷ 3,000 = ২০০ সেকেন্ড. মোট ৫ মিনিট outage-এর পর আরও ৩ মিনিট ২০ সেকেন্ড log দেরিতে আসবে। ওই সময়ে dashboard-এ পুরোনো ছবি দেখাবে, এটিও জানা থাকা দরকার।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি log line-এর যাত্রা সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- App stdout-এ JSON line লেখে
- Agent line-টি পড়ে metadata জোড়ে
- Buffer গতির ফারাক সামলায়
- Aggregator parse, redact আর sampling করে
- Store জমা রাখে
- Query দিয়ে খোঁজা হয়
Container-এ চলা একটি app-এর জন্য The Twelve-Factor App log লেখার কোন পদ্ধতি পরামর্শ দেয়?
- Container-এর ভেতরের ফাইলে লেখা আর নিজে rotation করা
- stdout-এ ঘটনার স্রোত লেখা, গন্তব্য ঠিক করবে চারপাশের ব্যবস্থা
- App থেকে সরাসরি log database-এ লেখা
- Production-এ সব log বন্ধ রাখা
Loki-তে কেন trace_id-কে label করা ভুল?
- কারণ label-এ শুধু সংখ্যা রাখা যায়
- কারণ trace id একটি secret
- কারণ প্রতিটি মান নতুন stream বানায়, এটি high cardinality
- কারণ label দিয়ে query করা যায় না
একটি system ২০০ request/s পায়, প্রতি request-এ ৪টি ৩৫০ byte-এর line লেখে। ৩০ দিনের মাসে আনুমানিক কত GB log হবে? (1 GB = 10⁹ byte)
- প্রায় ৭২৬ GB
- প্রায় ২৪ GB
- প্রায় ৭৩ GB
- প্রায় ৭,২৬০ GB
Log sampling-এর ক্ষেত্রে কোন নীতিটি সবচেয়ে ভালো?
- Error line-এর ১০% রাখা
- প্রতিটি line আলাদাভাবে এলোমেলো রাখা
- Audit log-এর ১% রাখা
- Error সব রাখা, আর info-র sampling trace id ধরে করা
Log agent-এ ১,০০০ line/s আসে। Store ১,৫০০ line/s নিতে পারে। ১,৮০,০০০ line-এর backlog শেষ হতে আনুমানিক কত সময় লাগবে?
- ১২০ সেকেন্ড
- ৩৬০ সেকেন্ড
- ১৮০ সেকেন্ড
- কখনো শেষ হবে না
Log-এ কার্ড নম্বর ঢুকেছে জানা গেল। সবচেয়ে টেকসই সমাধান কোনটি?
- শুধু আজকের log মুছে ফেলা
- শুধু একটি regex filter যোগ করে বিষয়টি শেষ ধরা
- Code-এ উৎস ঠিক করা, allowlist ও pipeline filter রাখা, আর জমা ডেটা ও ঝুঁকি সামলানো
- Retention বাড়িয়ে দেওয়া, যাতে তদন্ত সহজ হয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- App লেখে stdout-এ, JSON-এ, trace id সহ; গন্তব্য platform-এর দায়িত্ব।
- Pipeline: agent → buffer → aggregator → store → query. Store ধীর হলে চাপ উল্টো পথে ওঠে।
- Label হোক কম মানের (service, level, env); trace id থাকুক line-এর ভেতরে।
- খরচ কমান ঢোকার আগেই: debug বন্ধ, trace ধরে sampling, tier-ভিত্তিক retention. Error-এর sampling নয়।
- Log-এ PII আর secret নয়; স্তরে স্তরে redaction. Audit log আলাদা পথে, হারানো চলে না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৬৫ · Distributed tracing আর OpenTelemetry — span, context আর samplingপরবর্তী পর্বTrace id-র পুরো গল্প, span আর tail sampling.
- ক্লাউড কম্পিউটিং ও DevOps · কোর্স হোম৮১টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ৯ · Ops-এর shell — pipe, grep, awk, sed আর log বিশ্লেষণআবার দেখুনএকটি server-এর log পড়ার হাতিয়ার।
- System Design · Observabilityপাশের ট্র্যাকLog, metric আর trace-এর নকশার দিক।
- Operating Systems · Containerগভীরে যানContainer ভেতরে কীভাবে কাজ করে।