অধ্যায় 6 · LLMOps: লগিং, ট্রেসিং, খরচ ও মনিটরিং
স্ট্রাকচার্ড লগিং ও ট্রেসিং
- পৃষ্ঠা 11 / 22
- 23 মিনিট পড়া
বোগোতার মাতেও একজন গ্রাহকের বার্তা পেল: "আপনাদের অ্যাসিস্ট্যান্ট দুপুর ২টা ৫ মিনিটের দিকে আমাকে আবোলতাবোল বলেছে।" সে লগ খুলে একটাই লাইন পেল: ERROR request failed। কোন ধাপ ব্যর্থ হয়েছিল? উত্তরটা কি ধীর ছিল, খালি, মাঝপথে কাটা, নাকি প্রত্যাখ্যাত? কোন ডকুমেন্ট দেখেছিল? মডেল কি আদৌ ডাকা হয়েছিল? লাইনটা বলতে পারে না; আর পুরো প্রম্পট হয়তো বলতে পারত, কিন্তু তাতে গ্রাহকের কার্ড নম্বর আছে, তাই সেটা লগে থাকা চলে না। (এটা একটা বোঝানোর গল্প।)
এই পাতা অ্যাসিস্ট্যান্টকে এমন একটা স্মৃতি দেয় যা কাজের এবং নিরাপদ: স্ট্রাকচার্ড লগ ইভেন্ট, স্প্যান দিয়ে গড়া রিকোয়েস্ট ট্রেস, প্রতিটা ধাপের লেটেন্সি, টোকেন ও খরচ, আর কী কখনোই রেকর্ড হবে না তার নিয়ম। আমরা আগে নিজেদের ছোট একটা স্প্যান মডেল বানাই, যাতে ধারণাগুলো চোখে পড়ে, তারপর সেটা OpenTelemetry-র সঙ্গে মেলাই। সবকিছু অফলাইনে, ফেক ক্লক ও ফেক মডেলে চলে, তাই সংখ্যাগুলো পুনরাবৃত্তিযোগ্য; আসল মডেল ও নেটওয়ার্ক কম অনুমানযোগ্য, আর সেই কারণেই ইনস্ট্রুমেন্টেশন দরকার।
যা শিখবেন
- কীভাবে একটা রিকোয়েস্টকে রিট্রিভাল, টুল, কনটেক্সট, ক্যাশ, মডেল ও ভ্যালিডেশন জুড়ে ম্যাপ করবেন, এবং এমন আইডি দেবেন যা লগ, ট্রেস ও মূল্যায়ন কেসকে জোড়ে।
- কীভাবে নির্দিষ্ট স্কিমার স্ট্রাকচার্ড লগ ইভেন্ট লিখবেন, লগ করার সীমানায় রিডাকশন বসাবেন, এবং কোন ফিল্ড কখনোই নিরাপদ নয়।
- কীভাবে প্যারেন্ট-চাইল্ড স্প্যানে লেটেন্সি, টোকেন ও আনুমানিক খরচ রেকর্ড করবেন, JSON ফিক্সচার হিসেবে সংরক্ষণ করবেন, এবং ট্রি হিসেবে পড়বেন।
- কীভাবে ব্যর্থ রিকোয়েস্ট তার ট্রেস থেকে ডিবাগ করবেন, এবং ছোট নমুনায় ভরসা না করে ধীর বা ব্যর্থ ডিপেন্ডেন্সি ধরবেন।
- কীভাবে ঠিক করবেন কী ধরবেন, কতটা নমুনা নেবেন ও কতদিন রাখবেন, এবং আমাদের স্প্যান OpenTelemetry-তে কীভাবে ম্যাপ হয়, কনটেক্সট প্রোপাগেশনসহ।
কী নতুন, আর কী পুনর্ব্যবহার
কোনো বিদ্যমান ফাইল বদলায় না। নতুন সাবপ্যাকেজ evidencedesk/observe/-এ দশটা ফাইল: events, spans, privacy, pipeline, lab, viewer, debug, health, otel ও একটা __main__। ডেটা data/observe/-এ (সাতটা সিনারিও, ডিপেন্ডেন্সির সীমা ও একটা গোল্ডেন ট্রেস) এবং টেস্ট test_observe_*.py-এ। রিডাকশন নতুন করে লেখা হয়নি: events.py ডাকে প্রম্পট ইনজেকশন ও অ্যাক্সেসের সীমানা পাতার redact_log ও SecretGuard। খরচ আসে খরচ ও লেটেন্সির মূল কথা পাতার লেজার ও কাল্পনিক দামের টেবিল থেকে; ক্যাশ এবং তার হিট ও বাইপাস অবস্থা বাজেট, ক্যাশিং ও ব্যবহারের সীমা থেকে; স্ট্যাটাস টেবিল ও ফেইল-ক্লোজড নিয়ম নিরাপদ ব্যর্থতা ও নিরাপত্তা রিগ্রেশন টেস্ট থেকে। এজেন্ট লুপ, স্টেট ও মেমরি পাতার এজেন্ট ট্রেসের সিদ্ধান্ত-লাইন যেমন আছে তেমনই থাকে: সেই ট্রেস বেছে নেওয়া সিদ্ধান্ত রেকর্ড করে, এই স্প্যানগুলো রেকর্ড করে সময়।
এন্ড-টু-এন্ড রিকোয়েস্ট ট্রেস নির্ধারণ করুন
ট্রেস হলো একটা রিকোয়েস্টের গল্প। স্প্যান তার একটা ধাপ, যার শুরু, শেষ, প্যারেন্ট ও অ্যাট্রিবিউট আছে। আমাদের পাইপলাইনে ছয়টা ধাপ, আর কিছু ভুল হলে প্রতিটা আলাদা প্রশ্নের উত্তর দেয়:
| স্প্যান | যে প্রশ্নের উত্তর দেয় | সাধারণ ব্যর্থতার কোড |
|---|---|---|
retrieval | কী প্রমাণ পেলাম, আর সেরা স্কোর কেমন? | no_evidence |
tool | সতেজতা-যাচাইয়ের লুকআপ কি চলল, সফল হলো? | not_found, timeout |
context | যে রিকোয়েস্ট সাজালাম সেটা কত বড়? | input_too_large |
cache | হিট, মিস না বাইপাস? (মিস হলে ভেতরে মডেল স্প্যান থাকে।) | - |
model | কোন মডেল, কত টোকেন, থামার কারণ কী, খরচ কত? | timeout, rate_limited, server |
validation | রিপ্লাই কি পার্স হলো, সাইটেশন কি মিলল? | truncated, invalid_json, unknown_citation |
সবগুলো একটা রুট স্প্যান request-এর নিচে। চারটা আইডি সবকিছু বাঁধে: প্রান্তে (API-র x-request-id) বেছে নেওয়া রিকোয়েস্ট আইডি, ট্রেসার যে ট্রেস আইডি বানায়, এজেন্ট থাকলে রান আইডি, আর মূল্যায়ন সেট থেকে রিকোয়েস্ট এলে কেস আইডি। ব্যবহারকারীর আইডি এদের মধ্যে নেই; নিচে দেখুন।
স্ট্রাকচার্ড অ্যাপ্লিকেশন লগ বানান
লগ লাইন প্রথমে যন্ত্রের জন্য। "failed to answer for kwame"-এর মতো মুক্ত লেখা গোনা, ছাঁকা বা জোড়া যায় না। EventLog প্রতিটা ইভেন্টের জন্য একটা JSON অবজেক্ট লেখে, প্রতিবার একই কি (key) একই ক্রমে: ts, level, event, তিনটা আইডি, ছদ্মনামের user, duration_ms, outcome এবং বাকিটার জন্য একটা fields অবজেক্ট। ইভেন্টের নাম ছোট হাতের ও ডট-যুক্ত (request.start, span.end), তাই বানান-ভুলে এমন নতুন ইভেন্ট তৈরি হয় না যা খুঁজে পাওয়া যায় না। সময় আসে ইনজেক্ট করা ক্লক থেকে।
নিরাপদ লগিং ফিল্ড নির্ধারণ করুন
প্রতিটা ফিল্ড নিয়ে একবারই, কোডে সিদ্ধান্ত নিন। সবকিছু চার ধরনের একটা:
| ধরন | উদাহরণ | নিয়ম |
|---|---|---|
| মেটাডেটা | মডেলের নাম, টোকেন সংখ্যা, কোড, সময়কাল, হিট সংখ্যা | নির্দ্বিধায় লগ করুন। |
| শনাক্তকারী | ব্যবহারকারীর আইডি, টিকিট আইডি | স্থিতিশীল ছদ্মনাম লগ করুন (pseudonym(): সিক্রেট কি দিয়ে করা হ্যাশ, keyed hash), আসল মান কখনো নয়। |
| কনটেন্ট | প্রশ্ন, প্রম্পট, রিট্রিভ করা লেখা, উত্তর | শুধু আকার লগ করুন; লেখা অপ্ট-ইন, রিডাক্ট করা এবং অন্য জায়গায় রাখা। |
| সিক্রেট | API কি, বেয়ারার টোকেন, পাসওয়ার্ড | কখনো নয়। জানা মান ও কি-র আকৃতি সীমানাতেই মাস্ক করুন। |
গার্ড বসে emit-এর ভেতরে, কল-সাইটে নয়, কারণ যে কল-সাইটটা ভুলে যাওয়া হয় ফাঁসটা সেখান দিয়েই ঘটে:
# excerpt of kit/evidencedesk/observe/events.py (EventLog.emit)
clean: dict[str, Any] = {}
for name, value in sorted(fields.items()):
if name in NEVER_LOGGED:
clean[f"{name}_chars"] = len(str(value)) # the size is useful, the content is not
elif isinstance(value, str):
clean[name] = redact_log(value, self.guard)
elif isinstance(value, (int, float, bool)) or value is None:
clean[name] = value
else:
clean[name] = redact_log(json.dumps(value, sort_keys=True, default=str), self.guard)question, prompt, answer ইত্যাদি নামের ফিল্ড তার দৈর্ঘ্য দিয়ে বদলে যায়। অন্য লেখা redact_log-এর ভেতর দিয়ে যায়; লেখা নয় এমন মানও সিরিয়ালাইজ করে রিডাক্ট করা হয়, কারণ সিক্রেট ডিকশনারির ভেতরেও থাকতে পারে। দুটো ইভেন্ট: একটায় প্রশ্নে কার্ড নম্বর, আরেকটায় এরর ডিটেইলে একটা কি ও একটা বেয়ারার টোকেন:
import json
from evidencedesk.observe.events import EventLog
from evidencedesk.security import SIMULATED_SECRET, SecretGuard
from evidencedesk.usage import ManualClock
log = EventLog(ManualClock(1000.0), SecretGuard({"api_key": SIMULATED_SECRET}))
log.emit("request.start", request_id="req-001", user_id="kwame", model="fake-model",
question="My card 4111 1111 1111 1111 was charged twice")
log.emit("error.seen", level="warning", request_id="req-001", code="server",
detail=f"provider said: bad key {SIMULATED_SECRET}; Bearer abcdefgh12345")
for record in log.records():
print(json.dumps({k: v for k, v in record.items() if v not in ("", None)}))
{"ts": 1000.0, "level": "info", "event": "request.start", "request_id": "req-001", "user": "u-8a4a64f5", "fields": {"model": "fake-model", "question_chars": 45}}
{"ts": 1000.0, "level": "warning", "event": "error.seen", "request_id": "req-001", "fields": {"code": "server", "detail": "provider said: bad key [secret:[secret] Bearer [secret]"}}কার্ড নম্বর লাইনে পৌঁছায়নি, শুধু question_chars পৌঁছেছে। ব্যবহারকারী u-8a4a64f5, কোয়ামের জন্য প্রতিবার একই, আর সল্ট ছাড়া উল্টানো যায় না (এখানকার সল্ট ডেমো মান; আসলটা সিক্রেট)। কি-টা দেখুন: পুরোনো রিডাক্টর সেটা মাস্ক করেছে, কিন্তু তার প্লেসহোল্ডার [secret:api_key] আবার তার নিজের name=value নিয়মে বদলে গেছে। কিছু ফাঁস হয়নি, ফলাফলটা শুধু অগোছালো। আপনার নিজের ফরম্যাটে রিডাকশন পরীক্ষা করুন এবং আউটপুট পড়ুন; রিডাক্টর একটা প্যাটার্ন তালিকা, সম্পূর্ণ হতে পারে না, তাই প্রথম প্রতিরক্ষা হলো ফিল্ডটা লগই না করা। এক্সসেপশনের মেসেজ নয়, তার ক্লাস বা একটা স্থিতিশীল কোড রেকর্ড করুন: মেসেজে পথ, SQL ও কি থাকে।
লোকাল ট্রেস রিপ্রেজেন্টেশন বানান
Span-এ আছে ট্রেস আইডি, স্প্যান আইডি, প্যারেন্ট আইডি, নাম, শুরু ও শেষের সময়, স্ট্যাটাস, এরর কোড এবং অ্যাট্রিবিউটের একটা সমতল ডিকশনারি। Tracer.span(...) একটা কনটেক্সট ম্যানেজার। বর্তমান স্প্যান থাকে একটা ContextVar-এ, তাই একটা স্প্যানের ভেতরে খোলা স্প্যান তার চাইল্ড হয়, আর থ্রেড ও async কোডেও এটা কাজ করে; এটাই এক প্রসেসের ভেতরে কনটেক্সট প্রোপাগেশন।
# excerpt of kit/evidencedesk/observe/spans.py (Tracer.span)
def span(self, name: str, remote: dict[str, str] | None = None, **attrs: Any) -> Iterator[Span]:
parent, joined = self.current(), parse_traceparent((remote or {}).get("traceparent", ""))
trace_id, parent_id = (parent.trace_id, parent.span_id) if parent else joined or (self.ids.trace_id(), None)
span = Span(trace_id, self.ids.span_id(), parent_id, name, round(self.clock(), 6), attrs=dict(attrs), _tracer=self)
self.spans.append(span)
if "request_id" in span.attrs:
self.request_ids[trace_id] = str(span.attrs["request_id"])
token = self._current.set(span)
try:
yield span
except BaseException as exc:
span.status, span.error_type = "error", str(getattr(exc, "kind", type(exc).__name__)) # a stable code, never the message
raise
finally:
span.end = round(self.clock(), 6)
self._current.reset(token)
if self.log:
self.log.emit("span.end", level="error" if span.status == "error" else "info", trace_id=span.trace_id,
span_id=span.span_id, duration_ms=round(span.duration * 1000, 1), outcome=span.status,
request_id=self.request_ids.get(trace_id, ""), span=span.name)তিনটা বেরোনোর পথ পড়ুন। এক্সসেপশনে স্প্যান একটা স্থিতিশীল কোডসহ error চিহ্নিত হয় (এক্সসেপশনের ক্লাস, অথবা থাকলে তার kind, যেমন অ্যাডাপ্টার, কনফিগারেশন ও এরর হ্যান্ডলিং পাতার LLMError) এবং এক্সসেপশন ওপরে চলতে থাকে; finally সবসময় স্প্যান বন্ধ করে, তাই ক্র্যাশে স্প্যান খোলা পড়ে থাকে না। এক্সসেপশন নয় এমন ব্যর্থতা, যেমন প্রত্যাখ্যাত রিপ্লাই, span.fail(code) ডাকে। remote আর্গুমেন্ট একটা W3C traceparent হেডার নেয়, যার মাধ্যমে অন্য সার্ভিসের স্প্যান এই ট্রেসে যোগ দেয়; OpenTelemetry অংশে আমরা এটা ব্যবহার করব। এবার একটা আসল রিকোয়েস্ট, ফেক ক্লকে, data/observe/scenarios.jsonl-এর প্রথম সিনারিও থেকে:
from evidencedesk.observe import viewer
from evidencedesk.observe.lab import Lab, load_scenarios, run_scenario
rows = {r["id"]: r for r in load_scenarios()}
lab, reply = run_scenario(rows["ob01"])
trace = lab.tracer.last()
print(reply.status, reply.code, "| spans:", len(trace.spans), "| problems:", trace.problems())
print(viewer.tree(trace))answered ok | spans: 7 | problems: []
request +0.000s 0.486s code=ok model_calls=1 status=answered
├─ retrieval +0.000s 0.030s hits=3 k=3 top_score=0.56
├─ tool +0.030s 0.050s code=ok ok=True tool=lookup_document
├─ context +0.080s 0.004s evidence=3 input_tokens_est=326
├─ cache +0.084s 0.400s status=miss
│ └─ model +0.084s 0.400s cost=0.001446 model=fake-model tokens_in=326 tokens_out=39 usage_estimated=True
└─ validation +0.484s 0.002s code=okট্রি প্রথম কাজের ডিবাগিং টুল। অফসেট দেখায় ধাপগুলো পরপর চলে; cache স্প্যানের ভেতরে model স্প্যান, কারণ CachedClient শুধু মিসে মডেল ডাকে। problems() যাচাই করে ট্রেস ঠিকঠাক গড়া কি না: একটাই রুট, প্রতিটা প্যারেন্ট উপস্থিত, প্রতিটা চাইল্ড প্যারেন্টের সময়-জানালার ভেতরে। একই ট্রেস সাধারণ JSON ফিক্সচার; python -m evidencedesk.observe ob01 --json সেটা লেখে, আর একটা টেস্ট নতুন রানের সঙ্গে মেলায়, তাই ফরম্যাট নিঃশব্দে বদলাতে পারে না:
sed -n 1,18p kit/data/observe/trace_ob01.json{
"trace_id": "c0ffee00000000000000000000000001",
"spans": [
{
"trace_id": "c0ffee00000000000000000000000001",
"span_id": "5a00000000000001",
"parent_id": null,
"name": "request",
"start": 1000.0,
"end": 1000.486,
"status": "ok",
"error_type": "",
"attrs": {
"case_id": "ob01",
"code": "ok",
"model_calls": 1,
"question_chars": 36,
"request_id": "ob01",সময়গুলো ফেক-ক্লকের সেকেন্ড, আর অ্যাট্রিবিউটের কি সাজানো। ফিক্সচার লোড করলে একই যাচাই চলে, তাই হাতে-সম্পাদিত দুই-রুট বা এতিম-স্প্যানের ট্রেস প্রত্যাখ্যাত হয়।
পাইপলাইনের ধাপ ধরে লেটেন্সি মাপুন, তারপর টোকেন ও খরচ
প্রতিটা ধাপ একটা স্প্যান, তাই তার লেটেন্সি তার সময়কাল। ফেক ক্লক নির্দিষ্ট পরিমাণে এগোয়: রিট্রিভালে ০.০৩ সেকেন্ড, টুলে ০.০৫ সেকেন্ড, আর মডেলের জন্য খরচ ও লেটেন্সির মূল কথা পাতার SlowClient। আসল ধাপ বদলায়, তাই আসল সিস্টেমে পারসেন্টাইল দেখা হয়, নিচে। ব্যবহার করুন সেলফ টাইম, মানে সময়কাল বিয়োগ চাইল্ডদের সময়: cache স্প্যান ততক্ষণই চলে যতক্ষণ তার ভেতরের মডেল কল চলে, দুটো যোগ করলে একই সময় দুবার গোনা হয়। stage_table প্রতি ধাপের সেলফ টাইমের গড় ও সর্বোচ্চ দেখায়। চব্বিশটা কৃত্রিম রিকোয়েস্ট, যেখানে তেরোতম থেকে ফেক মডেল ধীর আর প্রতি চতুর্থ কল টাইমআউট:
from evidencedesk.observe.lab import synthetic_traffic
busy = Lab()
synthetic_traffic(busy) # 24 requests; from the 13th the fake model is slow and every fourth call times out
traces = busy.tracer.traces()
for name, window in [("first 12", traces[:12]), ("last 12", traces[12:])]:
print(name, [(stage, ms) for stage, _, ms, _ in viewer.stage_table(window)][:3])first 12 [('model', 400.0), ('tool', 50.0), ('retrieval', 30.0)]
last 12 [('model', 1625.0), ('tool', 50.0), ('retrieval', 30.0)]মডেলের গড় সেলফ টাইম ৪০০ মিলিসেকেন্ড থেকে ১৬২৫-এ ওঠে, অন্য ধাপ নড়ে না: ট্রেস বলে কোথায়, কেন নয়। মডেল স্প্যানে টোকেন ও খরচও থাকে। সেগুলো লেজারের একই UsageRecord ও cost_of থেকে ভরা, নতুন করে হিসাব নয়, তাই দুটো মতভেদ করতে পারে না। ক্যাশ হিটে মডেল স্প্যান থাকে না, লেজার রেকর্ডও না:
from decimal import Decimal
from evidencedesk.usage import cost_of
Q = rows["ob01"]["question"]
fresh = Lab()
fresh.ask(Q)
fresh.ask(Q) # the same question again
first, second = fresh.tracer.traces()
model = next(s for s in first.spans if s.name == "model")
rec = fresh.ledger.records[0]
print("span :", model.attrs["tokens_in"], model.attrs["tokens_out"], model.attrs["cost"])
print("ledger:", rec.input_tokens, rec.output_tokens, cost_of(rec).quantize(Decimal("0.000001")), "| estimated:", rec.estimated)
print("second:", [(s.name, s.attrs.get("status")) for s in second.spans if s.name in ("cache", "model")], "| ledger records:", len(fresh.ledger.records))span : 326 39 0.001446
ledger: 326 39 0.001446 | estimated: True
second: [('cache', 'hit')] | ledger records: 1এখানে টোকেন আনুমানিক (প্রতি টোকেনে ৪ অক্ষর), কারণ ফেক মডেল কিছু জানায় না, আর স্প্যান সেটা usage_estimated=True দিয়ে বলে দেয়। দাম কাল্পনিক দামের টেবিলের বানানো ক্রেডিট; আসল প্রোভাইডারের দাম আসবে বর্তমান প্রাইসিং পাতা থেকে। একটা সৎ ফাঁক: ব্যর্থ মডেল কল স্প্যান খোলে কিন্তু কোনো ব্যবহার থাকে না, তাই খরচ যোগ হয় না; অথচ বাজেট, ক্যাশিং ও ব্যবহারের সীমা পাতার call_billed ধরে নেয় ব্যর্থ পুনরায়-চেষ্টাযোগ্য প্রচেষ্টাও তার ইনপুটের খরচ করেছে। আপনার প্রোভাইডার ব্যর্থ প্রচেষ্টার বিল করলে সেগুলোও রেকর্ড করুন।
ব্যর্থ রিকোয়েস্ট ট্রেস করুন
সাতটা সিনারিও খোদ অবজার্ভেবিলিটির রিগ্রেশন সেট। প্রতিটায় একটা ফেক-মডেল মোড (পরিষ্কার রিপ্লাই, মাঝপথে কাটা, ভাঙা JSON, কখনো রিট্রিভ না-হওয়া সাইটেশন, টাইমআউট, ধীর রিট্রিভাল) এবং আমরা যে কোড, স্ট্যাটাস ও ব্যর্থ ধাপ আশা করি তা আছে। status নতুন নয়: এটা নিরাপদ ব্যর্থতা ও নিরাপত্তা রিগ্রেশন টেস্ট পাতার status_of, যা কেউ তালিকাভুক্ত করেনি এমন কোডকে ব্যর্থতা ধরে। debug.explain ট্রেস পড়ে, রুটের কোড নেয়, জানা কারণের টেবিলে ধাপটা খোঁজে এবং সবচেয়ে ধীর ধাপের নাম দেয়।
from evidencedesk.observe import debug
for row in load_scenarios():
lab, reply = run_scenario(row)
t = lab.tracer.last()
d = debug.explain(t)
print(f"{row['id']} {row['mode']:<14} {reply.code:<17} {reply.status:<8} stage={d.stage if d else '-':<10} total={t.root.duration:.3f}s")ob01 ok ok answered stage=- total=0.486s
ob02 ok no_evidence refused stage=retrieval total=0.030s
ob03 truncated truncated failed stage=validation total=0.486s
ob04 bad_json invalid_json failed stage=validation total=0.486s
ob05 unknown_cite unknown_citation failed stage=validation total=0.486s
ob06 timeout timeout failed stage=model total=2.084s
ob07 slow_retrieval ok answered stage=- total=1.686sসাতটার মধ্যে পাঁচটা উত্তর দেয়নি, আর তার মধ্যে একটাই (টাইমআউট) এক্সসেপশন তুলেছে। বাকিগুলো দেখতে ঠিক-ঠাক রিপ্লাই যা ভ্যালিডেশনে ব্যর্থ; সেজন্যই validation স্প্যান fail() দিয়ে ব্যর্থ চিহ্নিত হয়। যে রিকোয়েস্ট শুধু দীর্ঘ চলেছে (ob07, ধীর রিট্রিভাল) তা ব্যর্থতা নয় এবং ডায়াগনোজ হয় না; ধীরতা পরের অংশের কাজ। কমান্ড লাইন টাইমআউটটা সেভাবে দেখায় যেভাবে ডিবাগ করার সময় দেখা হয়:
python -m evidencedesk.observe ob06request +0.000s 2.084s code=timeout model_calls=1 status=failed
├─ retrieval +0.000s 0.030s hits=3 k=3 top_score=0.56
├─ tool +0.030s 0.050s code=ok ok=True tool=lookup_document
├─ context +0.080s 0.004s evidence=3 input_tokens_est=326
└─ cache +0.084s 2.000s ERROR=timeout
└─ model +0.084s 2.000s model=fake-model ERROR=timeout
request ob06: timeout (user saw: failed)
stage: model
cause: the model call did not answer in time
check: is it one request or every request? see dependency health
slowest stage: modelট্রি দেখায় মডেল স্প্যান ২.০ সেকেন্ড পরে ERROR=timeout নিয়ে বন্ধ হয়েছে, আর এক লাইন ওপরে সেটাকে ভেতরে রাখা cache স্প্যান। রিট্রিভাল ঠিক ছিল, তিনটা হিট, তাই প্রমাণ কারণ নয়। ট্রেস যা বলতে পারে না: প্রোভাইডার কেন ধীর ছিল, বা অন্য ব্যবহারকারীদেরও একই হয়েছিল কি না; সেজন্য নিচের ডিপেন্ডেন্সি দৃষ্টিকোণ লাগে। কারণের টেবিলটা পরের পরীক্ষার জন্য অনুমানের তালিকা, যা ইনসিডেন্ট থেকে বাড়ে।
লগ, ট্রেস ও মূল্যায়ন কেস মিলিয়ে দেখুন
মিলিয়ে দেখা মানে সব জায়গায় একই আইডি। প্রতিটা স্প্যান-শেষ লগ লাইনে রিকোয়েস্ট আইডি, ট্রেস আইডি ও স্প্যান আইডি থাকে; রুট স্প্যানে কেস আইডি; ব্যবহারকারী শুধু ছদ্মনামে। মূল্যায়নের একটা ব্যর্থতা থেকে রিকোয়েস্ট, ট্রেস ও তার লগ লাইন পাওয়া যায়, আর একটা এরর লাইন থেকে স্প্যান:
lab, _ = run_scenario(rows["ob05"])
trace = lab.tracer.last()
mine = [r for r in lab.log.records() if r["request_id"] == "ob05"]
print("eval case:", trace.root.attrs["case_id"], "| log lines:", len(mine), "| all share the trace id:", {r["trace_id"] for r in mine} == {trace.trace_id})
for r in mine:
print(f" {r['event']:<14} {r['fields'].get('span', ''):<10} {r['outcome']:<6} {r['level']}")
bad = next(r for r in mine if r["outcome"] == "error") # from a log line back to the span
print("first error line is span:", next(s.name for s in trace.spans if s.span_id == bad["span_id"]))eval case: ob05 | log lines: 9 | all share the trace id: True
request.start info
span.end retrieval ok info
span.end tool ok info
span.end context ok info
span.end model ok info
span.end cache ok info
span.end validation error error
request.end failed warning
span.end request ok info
first error line is span: validationrequest.end লাইন লেখা হয় রুট স্প্যানের ভেতরে, রুট বন্ধ হওয়ার আগে, তাই span.end request সবার শেষে। আপনার লগ আর ট্রেস আলাদা সিস্টেমে থাকলে এই জয়েন কি (join key) আপনার আর এক ঘণ্টার অনুমানের মাঝে একমাত্র জিনিস। প্রথম ইনসিডেন্টের আগেই আইডিগুলো ঠিক করুন, আর প্রতিটা স্তর নিজের আইডি বানাতে না দিয়ে রিকোয়েস্ট আইডি সব স্তরে পাঠান।
ঠিক করুন কী ধরবেন, কতটা নমুনা নেবেন, কতদিন রাখবেন
প্রম্পট, রিট্রিভ করা অংশ ও উত্তর ডিবাগিংয়ের সবচেয়ে কাজের এবং সবচেয়ে সংবেদনশীল ডেটা। CapturePolicy-র তিনটা মোড। off (ডিফল্ট) শুধু আকার রেকর্ড করে। inline স্প্যানে রিডাক্ট ও ছাঁটা লেখা বসায়; কৃত্রিম ডেটার প্রি-প্রোডাকশনে এটা চলে। external রিডাক্ট করা লেখা আলাদা স্টোরে রাখে, যার নিজস্ব অ্যাক্সেস নিয়ম ও ছোট মেয়াদ, আর স্প্যানে থাকে শুধু রেফারেন্স। একই রিকোয়েস্ট, তিন নীতি:
from evidencedesk.observe.privacy import EXTERNAL, INLINE, OFF
for name, policy in [("off", OFF), ("inline", INLINE), ("external", EXTERNAL)]:
lab = Lab(policy)
lab.ask(Q)
m = next(s for s in lab.tracer.spans if s.name == "model")
shown = {k: (v[:24] + "..." if isinstance(v, str) and len(v) > 24 else v) for k, v in m.attrs.items() if k.startswith(("prompt", "completion"))}
print(f"{name:<9}", shown, "| separate store:", len(lab.tracer.store.items))off {'prompt_chars': 839, 'completion_chars': 156} | separate store: 0
inline {'prompt_chars': 839, 'prompt_text': '<evidence source="kc-ret...', 'completion_chars': 156, 'completion_text': '{"answer": "You can retu...'} | separate store: 0
external {'prompt_chars': 839, 'prompt_ref': 'ref-0002', 'completion_chars': 156, 'completion_ref': 'ref-0003'} | separate store: 3নিরাপত্তা নোট (২০২৬-১০-১২ অনুযায়ী): OpenTelemetry GenAI কনভেনশন মডেলের নির্দেশ, ব্যবহারকারীর বার্তা ও মডেলের আউটপুটকে সংবেদনশীল বলে এবং বলে ইনস্ট্রুমেন্টেশন ডিফল্টে সেগুলো ধরবে না, কিন্তু একটা অপ্ট-ইন দেবে। তিনটা ধরন বর্ণিত: রেকর্ড না করা (ডিফল্ট), স্প্যান অ্যাট্রিবিউটে রাখা, অথবা বাইরে রেখে রেফারেন্স রাখা, যা সংবেদনশীল ডেটার প্রোডাকশনে সুপারিশ করা। উদাহরণ হিসেবে একটা এনভায়রনমেন্ট ভেরিয়েবল
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENTনামে এসেছে, সর্বজনীন সুইচ হিসেবে নয়। মডেলের নাম ও টোকেন সংখ্যার মতো মেটাডেটা সংবেদনশীল চিহ্নিত নয়। কোর SDK কনটেন্ট রিডাক্ট করে বা অপ্ট-ইন বাধ্য করে এমন কোনো বিবৃতি আমরা পাইনি, তাই ধরে নিন করে না। বর্তমান ডকুমেন্টেশন দেখে নিন।
প্রতিটা রিকোয়েস্টের ট্রেস রাখা সচরাচর সাধ্যে কুলোয় না, দরকারও নেই। Sampler ট্রেস আইডির হ্যাশ থেকে সিদ্ধান্ত নেয়, তাই একটা ট্রেসের সব স্প্যান একমত থাকে, আর একটা টেইল নিয়ম যোগ করে: যে ট্রেস ব্যর্থ বা ধীর ছিল তা হার যাই হোক রাখুন। টেইল নিয়মের জন্য শেষ হওয়া ট্রেস লাগে, তাই এটা প্রসেসের ভেতরে চলে; কালেক্টর পরে একই কাজ করতে পারে।
from collections import Counter
from evidencedesk.observe.privacy import Sampler
for sampler in [Sampler(rate=0.25), Sampler(rate=0.0)]:
print(sampler, sorted(Counter(sampler.decide(t)[1] for t in traces).items()))Sampler(rate=0.25, keep_errors=True, slow_s=None) [('dropped', 16), ('error', 3), ('sampled', 5)]
Sampler(rate=0.0, keep_errors=True, slow_s=None) [('dropped', 21), ('error', 3)]০.২৫ হারে ২১টা সুস্থ ট্রেসের পাঁচটা থাকে (আনুমানিক, ঠিক চারে একটা নয়) এবং তিনটা ব্যর্থতাই থাকে; ০ হারে শুধু ব্যর্থতাগুলো বাকি থাকে। নমুনা সাধারণ রিকোয়েস্টটাকে লুকিয়ে ফেলে, তাই নমুনা-করা ট্রেস থেকে হার হিসাব করবেন না; কাউন্টার ব্যবহার করুন (পরের পাতায়)। শেষে, কনটেন্ট মেটাডেটার চেয়ে কম সময় রাখুন: Retention-এ উদাহরণ মান আছে (কনটেন্ট ৩ দিন, মেটাডেটা ৩০ দিন), যা আপনার নিজের নীতির জায়গা-ধরা মান। লগ ও ট্রেসের রিটেনশন, মোছা ও অ্যাক্সেস ফিরে আসবে মনিটরিং, বাজেট ও ইনসিডেন্ট পাতায়।
ধীর বা ব্যর্থ ডিপেন্ডেন্সি ধরুন
রিট্রিভাল, টুল ও মডেল ডিপেন্ডেন্সি: আমাদের কোড ঠিক থাকলেও এরা ধীর বা ব্যর্থ হতে পারে। health.check প্রতিটার স্প্যান-সময়ের নিকটতম-র্যাংক ৯৫তম পারসেন্টাইল ও এরর হারকে data/observe/limits.json-এর সীমার সঙ্গে মেলায়, আর আটটার কম নমুনায় not_enough_data জানায়, কারণ তিনটা সংখ্যার পারসেন্টাইল মানে একটাই সংখ্যা। সীমাগুলো উদাহরণ, আপনার নিজের মাপে ঠিক করুন।
from evidencedesk.observe import health
from evidencedesk.observe.lab import DATA
limits = health.Limits.load(DATA / "limits.json")
for name, window in [("first 12", traces[:12]), ("last 12", traces[12:]), ("all 24", traces), ("first 3", traces[:3])]:
print(f"{name:<9}", [(f.stage, f.kind, f.observed, f.limit) for f in health.check(window, limits)])first 12 []
last 12 [('model', 'slow', 2.0, 1.0), ('model', 'failing', 0.25, 0.2)]
all 24 [('model', 'slow', 2.0, 1.0)]
first 3 [('retrieval', 'not_enough_data', 0.0, 8.0), ('tool', 'not_enough_data', 0.0, 8.0), ('model', 'not_enough_data', 0.0, 8.0)]প্রথম জানালা সুস্থ। শেষ বারোটা ধীর ও ব্যর্থ। পুরো-সময়ের জানালা ধীরতা এখনো দেখে কিন্তু ব্যর্থতা নয়, কারণ ২৪টায় তিনটা এরর মানে ১২.৫ শতাংশ, পাতলা হয়ে গেছে: দীর্ঘ জানালা নতুন সমস্যা লুকায়। সাম্প্রতিক জানালায় অ্যালার্ট করুন। তিনটা রিকোয়েস্ট কিছু বিচার করার জন্য খুব কম। কৃত্রিম ট্রাফিক কোড ঠিক কি না প্রমাণ করে; আপনার সীমা কী হওয়া উচিত তা বলে না।
OpenTelemetry-র ধারণার সঙ্গে পরিচিত হোন
OpenTelemetry ট্রেস, মেট্রিক ও লগের জন্য ভেন্ডর-নিরপেক্ষ একটা স্ট্যান্ডার্ড ও SDK। আমাদের মডেলে তার ধারণাগুলো আগেই আছে: প্যারেন্ট ও অ্যাট্রিবিউটসহ স্প্যান, রিকোয়েস্টের সব স্প্যান যে ট্রেস আইডি ভাগ করে, স্ট্যাটাস, আর কনটেক্সট প্রোপাগেশন (traceparent হেডার)। SDK যোগ করে ট্রেসার প্রোভাইডার যা ট্রেসার বানায়, স্প্যান প্রসেসর যা শেষ হওয়া স্প্যান এক্সপোর্টারকে দেয়, আর সার্ভিস সম্পর্কে রিসোর্স অ্যাট্রিবিউট। আমরা শুধু ইন-মেমরি এক্সপোর্টার ব্যবহার করি, তাই প্রসেসের বাইরে কিছু যায় না। আমাদের নাম থেকে মিলটা ছোট একটা টেবিল:
| আমাদের স্প্যান / অ্যাট্রিবিউট | OpenTelemetry |
|---|---|
request / retrieval / tool / model | gen_ai.operation.name = invoke_workflow / retrieval / execute_tool / chat |
| মডেল স্প্যানের নাম | {operation} {model}, যেমন chat fake-model; কাইন্ড CLIENT |
model, tokens_in, tokens_out, tokens_cached | gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.usage.cache_read.input_tokens |
tool | gen_ai.tool.name |
| এরর স্ট্যাটাস, এরর কোড | স্ট্যাটাস ERROR ও error.type |
বাকি সব (hits, cost ...) | আমাদের নিজের, app. উপসর্গে |
কনটেন্ট (*_text, *_ref) | এক্সপোর্ট হয় না |
টুল নোট (২০২৬-১০-১২ অনুযায়ী): কিট
opentelemetry-apiওopentelemetry-sdk1.45.1 পিন করে (প্রকাশ ২০২৬-১০-০৬, Apache-2.0, Python 3.10 বা তার পরের ভার্সন); এটা শুধুobserve/otel.py-এর ঐচ্ছিক ডিপেন্ডেন্সি।InMemorySpanExporterSDK-র সোর্সে আছে কিন্তু আমাদের পড়া ডকুমেন্টেশন পাতায় নেই; সেখানে ব্যাচ প্রসেসরসহ কনসোল এক্সপোর্টার দেখানো, আর টেস্টের জন্য সিনক্রোনাসSimpleSpanProcessorমানানসই। GenAI সেমান্টিক কনভেনশন এখন নিজস্ব রিপোজিটরিতে এবং স্ট্যাটাস Development, তাই ওপরের প্রতিটাgen_ai.*নাম বদলাতে পারে।gen_ai.tool.nameMCP কনভেনশন পাতায় আছে, তাই এখানে যেভাবে টুল স্প্যানে ব্যবহার হয়েছে সেটা ঠিক কি না VERIFY করুন।gen_ai.provider.nameরেজিস্ট্রির মান নেয়, যেমন প্রোভাইডারের নাম; আমাদের ফেক নিজের মান ব্যবহার করে। ভরসা করার আগে প্রতিটা নাম ও SDK কলের সিগনেচার বর্তমান ডকুমেন্টেশনে VERIFY করুন।
export শেষ হওয়া লোকাল ট্রেসকে আসল SDK-র ভেতর দিয়ে ফেক-ক্লকের টাইমস্ট্যাম্পসহ আবার চালায়, প্যারেন্ট আগে। ছাপা হয়েছে: ভালো রিকোয়েস্টের প্রধান স্প্যান, তারপর টাইমআউটের ব্যর্থ মডেল স্প্যান:
ATTRIBUTES = {
"model": {"model": "gen_ai.request.model", "tokens_in": "gen_ai.usage.input_tokens", "tokens_out": "gen_ai.usage.output_tokens",
"tokens_cached": "gen_ai.usage.cache_read.input_tokens"},
"tool": {"tool": "gen_ai.tool.name"},
}
OPERATION = {"request": "invoke_workflow", "retrieval": "retrieval", "tool": "execute_tool", "model": "chat"} # values of gen_ai.operation.name
PROVIDER = "evidencedesk.fake" # our own value for gen_ai.provider.name: a fake is not a provider in the registryfrom evidencedesk.observe import otel
provider, exporter = otel.make_provider()
otel.export(run_scenario(rows["ob01"])[0].tracer.last(), provider)
for s in exporter.get_finished_spans():
if s.name in ("request", "tool", "chat fake-model"):
print(f"{s.name} ({s.kind.name})")
print("".join(f" {k} = {v}\n" for k, v in sorted(s.attributes.items()) if k.startswith("gen_ai")), end="")
exporter.clear()
otel.export(run_scenario(rows["ob06"])[0].tracer.last(), provider)
spans = exporter.get_finished_spans()
bad = next(s for s in spans if s.status.status_code.name == "ERROR" and s.name.startswith("chat"))
parent = next(s.name for s in spans if s.context.span_id == bad.parent.span_id)
print(bad.name, bad.status.status_code.name, bad.attributes["error.type"], "| parent span:", parent)request (INTERNAL)
gen_ai.operation.name = invoke_workflow
tool (INTERNAL)
gen_ai.operation.name = execute_tool
gen_ai.tool.name = lookup_document
chat fake-model (CLIENT)
gen_ai.operation.name = chat
gen_ai.provider.name = evidencedesk.fake
gen_ai.request.model = fake-model
gen_ai.usage.cache_read.input_tokens = 0
gen_ai.usage.input_tokens = 326
gen_ai.usage.output_tokens = 39
chat fake-model ERROR timeout | parent span: cacheনাম, অপারেশনের মান, টোকেন অ্যাট্রিবিউট ও প্যারেন্ট-লিংক ঠিক পরিকল্পনামতো পৌঁছেছে; ব্যর্থ মডেল স্প্যানের স্ট্যাটাস ERROR ও error.type timeout, আর তার প্যারেন্ট cache স্প্যান। রিপ্লে করা শেখার সুবিধার শর্টকাট। আসল সার্ভিসে আপনি কাজ যেখানে ঘটে সেখানেই, আমাদেরটার বদলে OpenTelemetry API দিয়ে ট্রেসার ডাকতেন, আর with ব্লকের ধারণা একই। এবার প্রোপাগেশন: সার্ভিস A বর্তমান স্প্যানকে traceparent হেডারে লেখে (ভার্সন, ৩২-হেক্স ট্রেস আইডি, ১৬-হেক্স স্প্যান আইডি, ফ্ল্যাগ), সার্ভিস B সেটা পড়ে, আর B-র স্প্যান A-র ট্রেসে চাইল্ড হিসেবে যোগ দেয়:
hop = otel.propagation_demo(provider)
version, trace_id, span_id, flags = hop["header"].split("-")
print(version, len(trace_id), len(span_id), "| joined the same trace:", hop["same_trace"], "| parent is service-a:", hop["b_parent_is_a"])00 32 16 | joined the same trace: True | parent is service-a: Trueএই কৌশলেই একটা ট্রেস HTTP কল, কিউ মেসেজ বা সাবপ্রসেস পেরিয়ে যায়। OpenTelemetry একটা ট্রেসকে কাজের করে না: স্প্যানের নাম, অ্যাট্রিবিউট ও ওপরের গোপনীয়তার নিয়মই সেটা ঠিক করে।
মিনি-ল্যাব: একটা সম্পূর্ণ RAG রিকোয়েস্ট ট্রেস করুন
সম্পূর্ণ ল্যাব হলো একটা রিকোয়েস্ট, যাতে রিট্রিভাল, টুল, কনটেক্সট, ক্যাশ, মডেল ও ভ্যালিডেশন স্প্যান, প্রতি স্প্যানে একটা লগ লাইন, একটা JSON ফিক্সচার ও একটা টেক্সট ট্রি আছে। পাঁচটা টেস্ট ফাইল এটা ঢাকে: লগ স্কিমা ও রিডাকশন, স্প্যান মডেল ও প্রোপাগেশন, সিনারিও ও গোল্ডেন ট্রেস, হেলথ ও ডায়াগনোসিস, আর OpenTelemetry ম্যাপিং (SDK না থাকলে স্কিপ)। ঘড়ির সেকেন্ড sed দিয়ে বাদ দেওয়া হয়েছে:
python -m pytest kit/tests/test_observe_{events,spans,pipeline,health,otel}.py | tail -n 1 | sed 's/ in [0-9.]*s//'61 passedতারপর test_observe_pipeline.py খুলুন: একটা টেস্ট দেখায় ডিফল্ট নীতিতে কোনো ট্রেস বা লগে প্রশ্ন বা উত্তরের লেখা নেই, আরেকটা দেখায় লগ ও ট্রেস আইডি ভাগ করে, আরেকটা দেখায় গোল্ডেন ফাইল নতুন রানের সঙ্গে মেলে। এগুলো যা দেখাতে পারে না: আসল ধাপের সময় এই আকৃতির কি না, আসল মডেল শুধু এই সাত উপায়ে ব্যর্থ হয় কি না, বা আপনার রিডাকশন আপনার ডেটা ঢাকে কি না। প্রতিটা আসল ইনসিডেন্ট একটা নতুন সিনারিও সারি হয়।
কাজের জায়গায় এটা কোথায় দেখা যায়
ওসাকার আইকো একটা লজিস্টিকস কোম্পানির সাপোর্ট অ্যাসিস্ট্যান্ট চালায় (একটা বোঝানোর ঘটনা)। তার দল একবার লগ ফাইলে প্রম্পট পড়ে ধীর উত্তর ডিবাগ করেছিল; রিভিউতে তাতে গ্রাহকের ঠিকানা পাওয়া গেল। তারা এই নকশায় গেল: লগে আইডি ও আকার, কনটেন্ট ডিফল্টে বন্ধ, ব্যর্থতার ট্রেস সবসময় আর বাকিগুলোর বিশটা রিকোয়েস্টে একটা, আর নির্দিষ্ট ইনসিডেন্টের জন্য দুজনে খুলতে পারেন এমন স্বল্পমেয়াদি বাহ্যিক স্টোর। পরের ধীর-উত্তরের অভিযোগে দশ মিনিট লাগল: ট্রেস দেখাল ৯০ শতাংশ সময় রিট্রিভাল স্প্যানে।
সাধারণ ভুল
- "এখনকার জন্য" প্রম্পট লগ করা। কনটেন্ট ইনসিডেন্টের চেয়ে বেশিদিন থাকে এবং যাদের দেখার কথা নয় তাদের কাছে পৌঁছায়। আকার লগ করুন, অপ্ট-ইন ক্যাপচার ব্যবহার করুন, আর কনটেন্ট সাধারণ লগের বাইরে রাখুন।
- নেস্টেড সময়কাল যোগ করা। প্যারেন্ট তার চাইল্ডদের ধারণ করে। সেলফ টাইম দেখান, নইলে মডেল দুবার গুনবেন।
- প্রতি স্তরে নতুন আইডি। তাহলে লগ, ট্রেস ও মূল্যায়ন কেস জোড়া যায় না। আইডি প্রান্তে নিন এবং নিচে পাঠান।
- ছোট বা নমুনা-করা সেট থেকে বিচার। কয়েকটা বিন্দুর পারসেন্টাইল এবং নমুনা-করা ট্রেস থেকে হার বিভ্রান্ত করে। ন্যূনতম নমুনা ঠিক করুন, আর গুনতে কাউন্টার ব্যবহার করুন।
- স্মৃতি থেকে অ্যাট্রিবিউটের নাম বিশ্বাস করা। GenAI কনভেনশন Development-এ; নাম বর্তমান পাতা থেকে কপি করুন এবং আপনার ম্যাপিং পরীক্ষা করুন।
নিজে চেষ্টা করুন
- সহজ। খরচ ও লেটেন্সির মূল কথা পাতায় ফিরে যান। ২৪টা ট্রাফিক ট্রেসের সব মডেল স্প্যানের
costঅ্যাট্রিবিউট যোগ করুন এবং লেজারের মোটের সঙ্গে তুলনা করুন। অনুমান করুন মেলে কি না, এবং মডেল স্প্যান ও লেজার কল কয়টা। - মাঝারি। বাজেট, ক্যাশিং ও ব্যবহারের সীমা পাতায় ফিরে যান। একই ট্রেসে
Sampler(rate=0.0, slow_s=1.5)ব্যবহার করুন। অনুমান করুন কয়টা রাখা হবে, এবং প্রতিটার কারণ কী। - কঠিন। নিরাপদ ব্যর্থতা ও নিরাপত্তা রিগ্রেশন টেস্ট পাতায় ফিরে যান। একটা গোপনীয়তা-রিগ্রেশন চেক লিখুন: সাতটা সিনারিও চালিয়ে জানান কোনগুলো প্রশ্ন বা উত্তরের লেখা কোনো ট্রেস বা লগে ঢুকিয়েছে, একবার ডিফল্ট নীতিতে, একবার ইনলাইন ক্যাপচারে।
উত্তর
সহজ। মেলে, কারণ স্প্যান লেজারের খরচ কপি করে। মডেল স্প্যান ২৪টা কিন্তু লেজার কল ২১টা: টাইমআউট হওয়া তিনটা কল স্প্যান খুলেছে কিন্তু কোনো ব্যবহার রেকর্ড তৈরি করেনি।
total = sum(Decimal(s.attrs["cost"]) for t in traces for s in t.spans if s.name == "model" and "cost" in s.attrs)
print(total, busy.ledger.summary().cost.quantize(Decimal("0.000001")), "| model spans:", sum(s.name == "model" for t in traces for s in t.spans),
"| ledger calls:", busy.ledger.summary().calls)0.028044 0.028044 | model spans: 24 | ledger calls: 21মাঝারি। তিনটা ট্রেস ব্যর্থ (টাইমআউট), আর নয়টা ধীর-মডেল রিকোয়েস্ট চলে ১.৫৮৬ সেকেন্ড, ১.৫ সেকেন্ডের ওপরে, তাই ২৪টার ১২টা রাখা হয়: পুরো খারাপ অর্ধেক, আর কিছু নয়।
sampler = Sampler(rate=0.0, slow_s=1.5)
decisions = [sampler.decide(t) for t in traces]
print(sorted(Counter(reason for _, reason in decisions).items()), "| kept:", sum(keep for keep, _ in decisions))[('dropped', 12), ('error', 3), ('slow', 9)] | kept: 12কঠিন। ডিফল্ট নীতিতে কিছু ফাঁস হয় না। ইনলাইন ক্যাপচারে প্রতিটা সিনারিওতে ফাঁস হয়, তাই CI-তে চালানো এই চেক কাউকে ডিফল্টে সেটা চালু করতে দিত না। এটা শুধু আপনার দেওয়া নির্দিষ্ট লেখাগুলোই (needle) খোঁজে; জানা লেখার রিগ্রেশন টেস্ট, নিরাপত্তার প্রমাণ নয়।
def leaks(policy):
found = []
for row in load_scenarios():
lab, _ = run_scenario(row, policy)
blob = lab.tracer.last().to_json() + "".join(lab.log.lines)
found += [row["id"] for needle in (row["question"], "unridden") if needle in blob]
return found
print("default policy:", leaks(OFF))
print("inline capture:", sorted(set(leaks(INLINE))))default policy: []
inline capture: ['ob01', 'ob02', 'ob03', 'ob04', 'ob05', 'ob06', 'ob07']পাস চেকলিস্ট:
- সহজ: সমান মোট, ২৪ বনাম ২১, এবং তিনটা অনুপস্থিত রেকর্ডের ব্যাখ্যা দিতে পারেন।
- মাঝারি: ৩টা এরর আর ৯টা ধীর ট্রেস, ১২টা রাখা; টেইল নিয়মগুলোর নাম বলেছেন।
- কঠিন: ডিফল্টে খালি তালিকা, ইনলাইন ক্যাপচারে সাতটা আইডিই, এবং চেকটা যা দেখতে পারে না তা বলেছেন।
সারসংক্ষেপ
- ট্রেস একটা রিকোয়েস্টকে রিট্রিভাল, টুল, কনটেক্সট, ক্যাশ, মডেল ও ভ্যালিডেশন জুড়ে ম্যাপ করে; রিকোয়েস্ট আইডি, ট্রেস আইডি, রান আইডি ও কেস আইডি লগ, ট্রেস ও মূল্যায়ন কেস জোড়ে, আর ব্যবহারকারী থাকে শুধু ছদ্মনামে।
- নির্দিষ্ট স্কিমার স্ট্রাকচার্ড ইভেন্ট লগ করুন; রিডাকশন ও কখনো-লগ-না-করার তালিকা লগারের ভেতরেই প্রয়োগ করুন, আর এক্সসেপশনের মেসেজ নয়, কোড রাখুন।
- আগে লোকাল স্প্যান মডেল বানান: প্যারেন্ট-চাইল্ড, স্ট্যাটাস, অ্যাট্রিবিউট, JSON ফিক্সচার ও টেক্সট ট্রি। সেলফ টাইম ব্যবহার করুন, আর টোকেন ও খরচ নিন ব্যবহার রেকর্ড থেকে।
- ট্রেস থেকে ডিবাগ করুন: ব্যর্থ ধাপ খুঁজুন, তারপর কারণ যাচাই করুন। ডিপেন্ডেন্সি বিচার করুন সাম্প্রতিক জানালায়, ন্যূনতম নমুনা রেখে।
- কনটেন্ট ক্যাপচার অপ্ট-ইন, রিডাক্ট করা এবং বাহ্যিক হওয়াই ভালো; এমন নিয়মে নমুনা নিন যা ব্যর্থতা সবসময় রাখে; OpenTelemetry-র নাম বর্তমান পাতা থেকে ম্যাপ করুন এবং
traceparentদিয়ে কনটেক্সট পাঠান।
এরপর: মনিটরিং, বাজেট ও ইনসিডেন্ট এই ট্রেস ও লগকে কাউন্টার, ড্যাশবোর্ড, অ্যালার্ট, রিট্রাই, নিরাপদ ডিগ্রেডেশন, বাজেট সীমা ও ইনসিডেন্ট চেকলিস্টে রূপ দেয়।