
คำตอบสั้น: ส่ง trace context ไปกับงานแต่ละชิ้นใน queue envelope แล้วให้ worker สร้าง span ของตัวเองจาก parent ที่รับมา เก็บ job_id สำหรับค้นงานธุรกิจแยกจาก trace_id และ span_id สำหรับเชื่อมเหตุการณ์ การส่ง context เป็นข้อมูลสังเกตการณ์ ไม่ใช่หลักฐานยืนยันสิทธิ์หรือความถูกต้องของคำสั่ง
ลองนึกถึงงานทำวิดีโอสั้นที่ Agent coordinator รับคำขอ วางแผน เขียนสคริปต์ แล้วส่งงาน “สร้างภาพตัวอย่าง” ไปให้ worker ผ่านคิว หน้าจอ coordinator แสดงว่าเสร็จ แต่ worker กลับรายงาน error หาก log ของสองฝั่งมีเพียงข้อความและเวลา เราอาจต้องเดาว่า log ใดเป็นของงานนั้น แม้ทั้งสองระบบจะอยู่ใน trace เดียวกันได้ หาก trace identifier ไม่เคยเดินทางไปพร้อมข้อความที่ข้ามคิว
บทเรียนนี้ฝึกทักษะเดียว: propagate (ส่งต่อ) trace context ผ่าน envelope ของงานหนึ่งชิ้น เพื่อให้ค้น log ของ coordinator กับ worker เชื่อมกันได้ ตัวอย่างเป็น AUTHOR MOCK ด้วย Python 3.12 standard library: ใช้ queue.Queue ใน process เดียว สร้าง JSON log และจำลอง worker ไม่ได้เชื่อม broker, OpenTelemetry SDK/exporter, LLM, เครื่องมือจริง หรือ telemetry backend โค้ดและ acceptance criteria ในบทความนี้ยังเป็น NOT RUN; output ด้านล่างเป็นผลที่ออกแบบคาดหมาย ไม่ใช่ผลจากการรัน
ผลการเรียนรู้
หลังทำตาม คุณจะสร้าง envelope ที่มี business identifier กับ traceparent แยกช่อง, ตรวจรูปแบบรุ่น 00 ที่ตัวอย่างรองรับ, ส่ง parent context ไปให้ worker โดยไม่มี global current-trace และอ่าน log เพื่อแยก “งานธุรกิจนี้คืออะไร” ออกจาก “เหตุการณ์เหล่านี้สัมพันธ์กันอย่างไร” ได้
ทำไม job_id อย่างเดียวไม่พอ
job_id คือรหัสของงานตามกติกาธุรกิจ เช่น “สร้าง preview ของวิดีโอคำขอ J-204” ส่วน request_id มักใช้ระบุคำขอเข้า API หรือการสนทนาหนึ่งรอบ ทั้งสองชื่อเป็น convention ของแอปเราเอง รหัสเหล่านี้ช่วยค้นงานในฐานข้อมูลหรือตอบผู้ใช้ แต่ไม่ได้บอกโครงสร้าง parent-child ของการทำงานข้ามบริการ
trace_id ใช้รวม spans ที่สัมพันธ์กันใน distributed trace ส่วน span_id ระบุช่วงงานหนึ่งช่วง ใน W3C Trace Context ช่อง parent-id ระบุ request ฝั่ง caller ซึ่งหลายระบบแสดงเป็น span ID ดังนั้นเมื่อ coordinator เริ่มช่วง “ส่งงานเข้าคิว” มันควรสร้าง span ID ใหม่ แล้วใส่ span ID ของตัวเองเป็น parent ใน context ที่ส่งให้ worker; worker ค่อยสร้าง span ID ของตัวเอง ไม่ใช้ job_id แทนรหัสเหล่านี้ W3C Trace Context §3.2

ออกแบบ context ให้ตามงานไปโดยไม่ปนกัน
Context propagation หรือการส่งต่อบริบท คือการ serialize ค่า trace ที่จำเป็นก่อนข้ามขอบเขต แล้ว extract ค่าเหล่านั้นกลับเมื่อปลายทางรับข้อมูล OpenTelemetry อธิบายแนวคิดนี้สำหรับบริการและ execution unit ต่าง ๆ พร้อมกลไก propagator สำหรับระบบที่ติดตั้ง instrumentation จริง OpenTelemetry: Context propagation
ในการจำลอง เราส่ง traceparent เป็นช่องใน dataclass ที่ frozen แยกต่อ envelope แล้วส่ง envelope เข้า worker เป็น argument การทำเช่นนี้ลดการพึ่ง state ที่มีชีวิตร่วมกัน เช่น global current_trace ซึ่ง worker สองงานอาจเขียนทับกันจน log ของงาน A ได้ trace ของงาน B ข้อสรุปนี้เป็นการวิเคราะห์การออกแบบสำหรับ mock; ไม่ใช่ผลทดสอบ race condition และไม่ได้หมายความว่า envelope immutable จะทำให้ระบบจริงปลอดภัยครบถ้วน
ยิ่งกว่านั้น trace context ไม่ใช่ตัวตนของผู้ใช้และไม่ใช่สิทธิ์อนุมัติ tool call คนภายนอกอาจส่งค่าที่ปลอมได้ จึงต้องตรวจสิทธิ์และความถูกต้องของงานด้วยกลไกธุรกิจแยกต่างหาก อย่าอนุญาตให้ traceparent ทำให้ worker ข้าม approval gate เปลี่ยนเจ้าของงาน หรือเข้าถึง resource เพิ่มขึ้น
ขอบเขต parser รุ่น 00
รูปแบบ traceparent รุ่น 00 มี version 2 ตัว, trace ID 32 ตัว, parent ID 16 ตัว และ flags 2 ตัว โดยคั่นด้วยขีด ใช้เลขฐาน 16 ตัวเล็กตามรูปแบบนี้ และ trace ID กับ parent ID ต้องไม่เป็นศูนย์ทั้งหมด parser ในตัวอย่างรับเฉพาะรูปแบบนี้ จึงปฏิเสธ version อื่นและรูปแบบผิด; นี่ไม่ใช่ parser สำหรับ future versions หรือ implementation ครบถ้วนของ W3C
Flags มี 2 hex characters แต่ความหมายของ bit เพิ่มเติมสงวนไว้ตาม W3C Recommendation รุ่น Level 1; เมื่อสร้าง traceparent รุ่น 00 ใหม่ ตัวอย่างคงเฉพาะ sampled bit ต่ำสุดและเขียน reserved bits เป็นศูนย์ ไม่ส่งต่อค่า flags ที่ไม่รู้จักแบบไม่ตรวจ W3C Trace Context §3.2.2.5.2 การรับค่าที่มี reserved bits ตั้งไว้กับการสร้างค่าขาออกจึงเป็นคนละขั้นกัน
เมื่องานมาถึงโดยไม่มี context หรือ context ใช้ไม่ได้ worker เริ่ม trace ใหม่พร้อมระบุเหตุผล เช่น missing_context หรือ invalid_context ใน log การเริ่ม trace ใหม่เป็นนโยบายของตัวอย่างเพื่อไม่ให้ข้อมูลสังเกตการณ์ที่ผิดรูปทำให้ธุรกิจหยุดทำงาน แต่ใน production ยังต้องมี schema validation, authentication/authorization และนโยบายรับหรือกักข้อความตามข้อกำหนดของงาน หาก job เองไม่ผ่าน validation ให้ปฏิเสธตามระบบงาน ไม่ใช่ใช้ trace parser ตัดสินแทน

เมื่อไม่มี context หรือค่าผิดรูป จุดสำคัญคือแยกการ restart trace ออกจากการตัดสินสิทธิ์และการปฏิเสธ job การทำงานธุรกิจยังต้องผ่านระบบยืนยันตัวตน ตรวจ schema และ authorization ของตัวเอง:

ตัวอย่างจำลองที่อ่านโครงสร้าง log ได้
ไฟล์ code/trace_context_mock.py เป็นตัวอย่างการออกแบบ ไม่ได้เชื่อมระบบภายนอก ตัว coordinator สร้าง trace ID และ span ID แบบสุ่มที่ไม่เป็นศูนย์ สร้าง traceparent โดยให้ span ของ coordinator เป็น parent แล้วบรรจุพร้อม job_id และ request_id ลง envelope ที่แก้ค่าไม่ได้ จากนั้น worker รับ envelope, parse รุ่น 00, สร้าง span ใหม่ และบันทึก event ว่า context พร้อมสำหรับ mock เท่านั้น
ข้อกำหนดที่ตั้งใจให้ทำซ้ำได้: Python 3.12 ขึ้นไป, standard library เท่านั้น, ไม่ต้องติดตั้งแพ็กเกจ, ไม่มี environment variable และใช้ fixtures video-204/video-205 ในตัวอย่าง ไม่มี network หรือ external/paid API call และไม่มีค่าใช้จ่ายจาก API ที่ตัวอย่างเรียก; ต้นทุน telemetry หรือ production ยังไม่ได้วัด
ตัวอย่างเตรียมสองงานแล้วหยิบออกจากคิวในลำดับกลับกันเพื่อให้เห็นว่าข้อมูล context ติดกับ envelope ของงานใดงานหนึ่ง ไม่ได้อ่านค่าจาก “trace ปัจจุบัน” ตัวเดียว ผลนี้แสดงการออกแบบการสลับลำดับแบบกำหนดไว้ในโค้ด ไม่ใช่การรันพร้อมกันจริงหรือการพิสูจน์ว่าไม่มี race ในระบบ worker หลาย process
Log เป็น JSON ที่มี event, service, job_id, request_id, trace_id, span_id, parent_span_id และ status ตามเหตุการณ์ แต่ไม่บันทึก prompt, ผลลัพธ์ tool, token, secret, PII หรือ baggage การออกแบบนี้สอดคล้องกับแนวคิดที่ OpenTelemetry Logs Data Model มีช่อง TraceId/SpanId/TraceFlags เพื่อเชื่อม log กับ trace แต่ JSON dictionary นี้ไม่ใช่ OpenTelemetry LogRecord และยังไม่ได้ส่งออกไป backend OpenTelemetry Logs Data Model
ตัวอย่าง output ที่คาดหมายมีสี่ records: coordinator ยอมรับ envelope สองงานเข้าคิว และ worker สอง records ที่ context พร้อมประมวลผล (ค่าจริงจะสุ่มเมื่อรัน; ด้านล่างจงใจย่อ ID เพื่อให้อ่านง่าย จึงไม่ใช่ traceparent ที่นำไป parse ได้):
{"event":"job.enqueued","job_id":"video-204","trace_id":"trace-A","span_id":"coord-A","parent_span_id":null}
{"event":"job.enqueued","job_id":"video-205","trace_id":"trace-B","span_id":"coord-B","parent_span_id":null}
{"event":"job.context_ready","job_id":"video-205","trace_id":"trace-B","span_id":"worker-B","parent_span_id":"coord-B","status":"mock_context_only"}
{"event":"job.context_ready","job_id":"video-204","trace_id":"trace-A","span_id":"worker-A","parent_span_id":"coord-A","status":"mock_context_only"}
อ่านว่าแต่ละ worker รักษาคู่ job_id/trace_id ของ envelope แม้ลำดับกลับกัน และมี span ใหม่ที่อ้าง parent ของ coordinator ค่านี้เป็น expected output ที่ออกแบบไว้ ไม่มีผลรันยืนยัน และไม่ได้บอกว่า preview job เสร็จ
โค้ดเต็มที่ใช้เป็นแบบจำลองอยู่ด้านล่าง; อ่านได้โดยไม่ต้องติดตั้ง dependency เพิ่มเติม แต่ยังไม่ควรรันหรือถือว่าผ่านการตรวจโดยอัตโนมัติ:
ขอบเขตข้อมูลเข้าของ mock คือ envelope ที่สร้างจาก fixture และมีช่องข้อความตามที่โค้ดคาดไว้: type annotation ไม่ตรวจ schema ขณะรัน และ parser นี้รับ str เท่านั้น ระบบจริงต้องตรวจชนิด/ความยาวและที่มาของงานก่อนถึงขั้นนี้ frozen=True ช่วยห้ามเปลี่ยนค่าช่องตามวิธีปกติ แต่ไม่ได้ทำ deep immutability ให้ข้อมูลซ้อนทุกชนิด ตัวอย่างนี้เก็บช่องเป็นข้อความทั้งหมด Python dataclasses
queue.Queue() ในตัวอย่างไม่จำกัดความจุและรับเพียงสอง fixture ที่กำหนดไว้ จึงไม่ได้สอนหรือรับรองการรับโหลดจริง คิวนี้เป็นโครงสร้างใน process เดียว ไม่มี durable delivery หรือ broker ข้ามเครื่อง การกำหนด capacity และเงื่อนไขรับงานเป็นงานออกแบบอีกส่วนหนึ่ง Python queue
code/trace_context_mock.py · NOT RUN
"""AUTHOR MOCK: per-job W3C traceparent v00 propagation using Python 3.12 stdlib.
No broker, OpenTelemetry SDK/exporter, LLM, tools, or telemetry backend.
This file and its companion tests are NOT RUN. The JSON events are illustrative.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
import queue
import re
import secrets
from typing import Any
TRACEPARENT_V00 = re.compile(
r"^(?P<version>00)-(?P<trace_id>[0-9a-f]{32})-"
r"(?P<parent_id>[0-9a-f]{16})-(?P<flags>[0-9a-f]{2})$"
)
ZERO_TRACE_ID = "0" * 32
ZERO_SPAN_ID = "0" * 16
@dataclass(frozen=True)
class TraceParent:
trace_id: str
parent_id: str
flags: int
def _nonzero_hex(nbytes: int) -> str:
"""Return a lowercase random ID string that is not all zeroes."""
while True:
value = secrets.token_hex(nbytes)
if int(value, 16) != 0:
return value
def parse_traceparent(value: str) -> TraceParent | None:
"""Parse only the fixed-width, lowercase traceparent version 00 format."""
match = TRACEPARENT_V00.fullmatch(value)
if match is None:
return None
trace_id = match.group("trace_id")
parent_id = match.group("parent_id")
if trace_id == ZERO_TRACE_ID or parent_id == ZERO_SPAN_ID:
return None
return TraceParent(
trace_id=trace_id,
parent_id=parent_id,
flags=int(match.group("flags"), 16),
)
def format_traceparent(trace_id: str, parent_id: str, flags: int) -> str:
"""Format v00, keeping only the sampled bit and zeroing reserved bits."""
if len(trace_id) != 32 or not re.fullmatch(r"[0-9a-f]{32}", trace_id):
raise ValueError("trace_id must be 32 lowercase hex characters")
if len(parent_id) != 16 or not re.fullmatch(r"[0-9a-f]{16}", parent_id):
raise ValueError("parent_id must be 16 lowercase hex characters")
if trace_id == ZERO_TRACE_ID or parent_id == ZERO_SPAN_ID:
raise ValueError("trace_id and parent_id must be non-zero")
# In the stable W3C v00 baseline, only bit 0 is defined (sampled).
normalized_flags = flags & 0x01
return f"00-{trace_id}-{parent_id}-{normalized_flags:02x}"
@dataclass(frozen=True)
class JobEnvelope:
"""Small immutable carrier; business IDs are not trace identifiers."""
job_id: str
request_id: str
traceparent: str
task_name: str
def emit_log(**fields: Any) -> None:
"""Print metadata-only JSON; callers must not pass job payloads or secrets."""
print(json.dumps(fields, ensure_ascii=False, sort_keys=True))
class Coordinator:
def __init__(self, work_queue: queue.Queue[JobEnvelope]) -> None:
self.work_queue = work_queue
def enqueue(self, job_id: str, request_id: str, task_name: str) -> JobEnvelope:
trace_id = _nonzero_hex(16)
coordinator_span_id = _nonzero_hex(8)
# A new traceparent carries this coordinator span as the downstream parent.
traceparent = format_traceparent(trace_id, coordinator_span_id, flags=1)
envelope = JobEnvelope(job_id, request_id, traceparent, task_name)
self.work_queue.put(envelope)
emit_log(
event="job.enqueued",
service="coordinator",
job_id=job_id,
request_id=request_id,
trace_id=trace_id,
span_id=coordinator_span_id,
parent_span_id=None,
status="queued",
)
return envelope
class Worker:
def process(self, envelope: JobEnvelope) -> dict[str, Any]:
incoming = parse_traceparent(envelope.traceparent)
restart_reason: str | None = None
if incoming is None:
# Observability context failure does not authorize or invalidate a job.
# Business authentication and schema checks belong to separate controls.
restart_reason = (
"missing_context" if not envelope.traceparent else "invalid_context"
)
trace_id = _nonzero_hex(16)
parent_span_id = None
flags = 0
else:
trace_id = incoming.trace_id
parent_span_id = incoming.parent_id
flags = incoming.flags & 0x01
worker_span_id = _nonzero_hex(8)
while worker_span_id == parent_span_id:
worker_span_id = _nonzero_hex(8)
# The worker creates its own span; its ID is not the incoming parent ID.
outgoing = format_traceparent(trace_id, worker_span_id, flags)
event: dict[str, Any] = {
"event": "job.context_ready",
"service": "video-preview-worker",
"job_id": envelope.job_id,
"request_id": envelope.request_id,
"trace_id": trace_id,
"span_id": worker_span_id,
"parent_span_id": parent_span_id,
"status": "mock_context_only",
}
if restart_reason:
event["trace_restart_reason"] = restart_reason
emit_log(**event)
return {"traceparent": outgoing, **event}
def main() -> None:
jobs: queue.Queue[JobEnvelope] = queue.Queue()
coordinator = Coordinator(jobs)
worker = Worker()
coordinator.enqueue("video-204", "request-77", "create_preview")
coordinator.enqueue("video-205", "request-78", "create_preview")
# Deterministic reverse dequeue illustrates context isolation by envelope.
# It is sequential in-process behavior, not concurrent worker execution.
pending = [jobs.get_nowait(), jobs.get_nowait()]
for envelope in reversed(pending):
worker.process(envelope)
if __name__ == "__main__":
main()
code/test_trace_context_mock.py · NOT RUN
"""AUTHOR MOCK test design; intentionally NOT RUN."""
import io
import unittest
from contextlib import redirect_stdout
from trace_context_mock import (
JobEnvelope,
Worker,
format_traceparent,
parse_traceparent,
)
class TraceContextMockTests(unittest.TestCase):
def test_worker_span_is_child_of_parent_and_keeps_business_ids(self):
trace_id = "1234567890abcdef1234567890abcdef"
parent_id = "1234567890abcdef"
envelope = JobEnvelope(
"video-1", "request-1", f"00-{trace_id}-{parent_id}-01", "preview"
)
result = Worker().process(envelope)
self.assertEqual(result["job_id"], "video-1")
self.assertEqual(result["request_id"], "request-1")
self.assertEqual(result["trace_id"], trace_id)
self.assertEqual(result["parent_span_id"], parent_id)
self.assertNotEqual(result["span_id"], parent_id)
def test_parser_scope_and_invalid_identifiers(self):
tid = "1234567890abcdef1234567890abcdef"
sid = "1234567890abcdef"
self.assertIsNotNone(parse_traceparent(f"00-{tid}-{sid}-ff"))
self.assertIsNone(parse_traceparent(f"00-{tid.upper()}-{sid}-01"))
self.assertIsNone(parse_traceparent(f"01-{tid}-{sid}-01"))
self.assertIsNone(parse_traceparent(f"00-{'0' * 32}-{sid}-01"))
self.assertIsNone(parse_traceparent(f"00-{tid}-{'0' * 16}-01"))
self.assertIsNone(parse_traceparent(f"00-{tid}-{sid}-gg"))
self.assertIsNone(parse_traceparent(""))
def test_outgoing_reserved_flags_are_zeroed(self):
tid = "1234567890abcdef1234567890abcdef"
sid = "1234567890abcdef"
self.assertEqual(format_traceparent(tid, sid, 0xFF), f"00-{tid}-{sid}-01")
self.assertEqual(format_traceparent(tid, sid, 0xFE), f"00-{tid}-{sid}-00")
def test_missing_or_invalid_context_restarts_trace_with_reason(self):
worker = Worker()
missing = worker.process(JobEnvelope("j1", "r1", "", "preview"))
invalid = worker.process(JobEnvelope("j2", "r2", "not-a-traceparent", "preview"))
self.assertEqual(missing["trace_restart_reason"], "missing_context")
self.assertEqual(invalid["trace_restart_reason"], "invalid_context")
self.assertIsNone(missing["parent_span_id"])
self.assertNotEqual(missing["trace_id"], invalid["trace_id"])
def test_envelopes_keep_context_isolated_when_processed_out_of_order(self):
tids = ["1" * 32, "2" * 32]
parents = ["a" * 16, "b" * 16]
envelopes = [
JobEnvelope(f"job-{i}", f"req-{i}", f"00-{tids[i]}-{parents[i]}-01", "preview")
for i in range(2)
]
results = [Worker().process(item) for item in reversed(envelopes)]
by_job = {result["job_id"]: result for result in results}
for i in range(2):
self.assertEqual(by_job[f"job-{i}"]["trace_id"], tids[i])
self.assertEqual(by_job[f"job-{i}"]["parent_span_id"], parents[i])
def test_worker_log_does_not_contain_task_payload(self):
# The task name stands in for sensitive job content and must not be logged.
output = io.StringIO()
envelope = JobEnvelope(
"job-3", "req-3", "", "sensitive-prompt-marker-do-not-log"
)
with redirect_stdout(output):
Worker().process(envelope)
self.assertNotIn("sensitive-prompt-marker-do-not-log", output.getvalue())
self.assertNotIn("payload", output.getvalue())
if __name__ == "__main__":
unittest.main()
code/README.md · NOT RUN
# Trace Context queue mock (AUTHOR MOCK)
**Status: NOT RUN.** Intended for Python 3.12 or later, standard library only. No package installation or environment variables are needed. Input fixtures are fixed business IDs (`video-204`, `video-205`, corresponding request IDs, and a preview task label); IDs used in trace context are generated locally. This is an in-process `queue.Queue` carrier mock; it has no broker, OpenTelemetry SDK/exporter, LLM, real tool, telemetry backend, or production integration. It makes no network or external/paid API calls. No API-call cost is incurred by this example; production telemetry costs were not measured.
From this directory, the intended commands are:
```text
python trace_context_mock.py
python -m unittest -v test_trace_context_mock.py
```
These commands are documentation only and were not executed. If run in an explicitly authorized later task, expect four JSON records: two coordinator `job.enqueued` records after queue admission, followed by two worker `job.context_ready` records in reverse order. The random trace/span IDs will differ. Each worker record should preserve that envelope's `job_id` and `trace_id`, use a new worker `span_id`, and set `parent_span_id` to the coordinator span ID. `mock_context_only` means the trace context was parsed and a mock worker span was prepared; it does not mean the preview job ran or completed. The tests describe intended assertions; no test result is claimed.
The parser intentionally accepts only lowercase fixed-width traceparent version `00`, rejects zero trace/span IDs, and accepts any two-hex-digit incoming flags. When producing a new version-00 value it retains the sampled bit and zeros reserved flag bits according to the stable W3C Trace Context Recommendation baseline. It is not a complete/future-version W3C parser. Bad or missing tracing metadata starts a new trace with a reason; it does not authenticate or authorize a business job.
The envelope contains metadata only and is frozen. A production carrier must separately define serialization, broker headers/properties, size limits, schema evolution, retry and duplicate semantics, authentication/authorization, and sensitive-data policy. The sequential reverse-order illustration does not prove concurrency safety or race behavior. No prompt, tool payload, credentials, PII, or baggage should be added to logs by copying this mock without review.
หากนำไปลองในภายหลัง ให้เปิด terminal ในโฟลเดอร์ code และใช้คำสั่งที่ระบุใน README:
python trace_context_mock.py
python -m unittest -v test_trace_context_mock.py
คำสั่งข้างต้นเป็นวิธีใช้ที่บันทึกไว้เท่านั้น ยังไม่ได้รัน ผลคาดหมายคือสี่ JSON records ตามตัวอย่างก่อนหน้า ไม่ใช่ผลสังเกตจริง และการทดสอบยัง NOT RUN
การออกแบบสำหรับระบบจริง
ในระบบจริงให้เลือก OpenTelemetry instrumentation/propagator ที่ตรงกับ framework หรือ broker และ serialize carrier ตาม transport การใช้ traceparent ใน message property เป็น integration decision; ตรวจ library และเวอร์ชันก่อนนำไปใช้
เอกสาร OpenTelemetry messaging semantic conventions ที่อ้างอิงในงานวิจัยนี้มีสถานะ Development และยังไม่กำหนดวิธีแนบ/ดึง context สำหรับทุก transport แบบตายตัว แนวคิดทั่วไปให้ correlate producer กับ consumer; แต่ batch, fan-out, retry และ consumer ที่มี ambient context อาจต้องใช้ span links แทนการบังคับให้ทุก consumer เป็น child ของ parent เดียว บทเรียนนี้จำกัดตัวเองที่หนึ่ง envelope ต่อหนึ่ง worker และ parent-child เพื่อสอนเส้นทางพื้นฐาน Messaging spans
สำหรับงานจริงให้แยกสิทธิ์ของผู้ส่ง/worker, business validation, ความต่อเนื่องของ trace และนโยบายเก็บข้อมูลออกจากกัน ค่า trace ไม่ควรเป็น idempotency key: การเริ่ม trace ใหม่ไม่ได้ทำให้งานธุรกิจเป็นงานใหม่โดยอัตโนมัติ
ก่อนเพิ่มข้อมูลใน baggage ให้ถามว่าจำเป็นต่อทุก downstream service จริงหรือไม่ OpenTelemetry เตือนให้ระวังการเปิดเผยข้อมูลผ่าน baggage และ context ที่มาจากภายนอก การ propagate เฉพาะ traceparent ก็ช่วยลดข้อมูลที่ส่งต่อ แต่ไม่ได้ยืนยันว่าระบบปลอดภัยหรือ log ไม่มีข้อมูลอ่อนไหวโดยอัตโนมัติ OpenTelemetry security best practices
บทเรียนนี้โฟกัสการส่ง context ต่อหนึ่งงาน ไม่ได้สอนการออกแบบระบบ queue ทั้งระบบ หากต้องทบทวนภาพสถาปัตยกรรม workflow/single/multi-agent ให้อ่าน บทความ AI Agent Architecture; ส่วนการจำกัดคิวแยกตาม dependency เพื่อไม่ให้งานค้างลาม ดู บทความ Bulkhead และ bounded queue ซึ่งเป็นอีกทักษะหนึ่ง ไม่ได้แทน trace propagation
Video Lab: ฝึกเขียน brief ให้แต่ละช่วงส่งต่อ trace อย่างไร
ใช้ Lab เป็นแบบจำลองวางงานวิดีโอสั้น แล้วเขียน task brief ให้ coordinator, queue และ worker ส่งต่อ context ด้วย envelope ของงานแต่ละชิ้น จุดสังเกตคือทุกขั้นยังใช้ job_id เดิม แต่แต่ละ span มีตัวเอง และ worker ต้องอ่าน context จากข้อความของตน อย่าให้ Lab สร้างหรืออ้างว่าตั้งค่า tracing จริง เครื่องมือนี้เป็นวิดีโอเวิร์กโฟลว์จำลอง ไม่ได้ปล่อย trace หรือเชื่อม broker
ก่อนคัดลอก prompt จาก Lab เติมส่วนที่ขาดให้ครบหกด้าน: เป้าหมาย, บริบท/ข้อมูล, เครื่องมือและสิทธิ์, ขอบเขต, สิ่งส่งมอบและเกณฑ์ตรวจรับ, จุดหยุด/การส่งต่อเมื่อ context ผิด รวมถึงระบุว่า trace context ใช้สังเกตการณ์เท่านั้น ค่าระยะเวลาและค่าใช้จ่ายใน Lab เป็นสมมติฐานที่ปรับได้ ไม่ใช่ผลวัดของระบบจริง
จากคลิปดิบ
สู่จักรวาลคอนเทนต์
ChatGPT ตัดต่อวิดีโอแทนคนได้จริงแค่ไหน? สำรวจระบบ AI Video Editing Agent ผ่านฉากสามมิติที่โต้ตอบได้ พร้อมแยกข้อเท็จจริง สร้างพรอมป์ต์ และจำลองต้นทุนก่อนลงมือทำจริง
ออกแบบระบบตัดต่อในมุมมอง 3D
ลากบนฉากเพื่อหมุนมุมมอง แตะโหนดเพื่อดูหน้าที่ของ AI ในแต่ละขั้นตอน
จัดระเบียบไฟล์ ตรวจคุณภาพภาพและเสียง และกำหนดสิทธิ์เข้าถึงให้ AI Agent
แยกความสามารถจริงจากคำโฆษณา
กดแต่ละประเด็นเพื่ออ่านรายละเอียดและแหล่งข้อมูล
ChatGPT Work เข้าถึงไฟล์ในเครื่องได้ตรวจสอบแล้ว / มีเงื่อนไข+
Astra ใช้โปรแกรมคอมพิวเตอร์ทำงานได้ตรวจสอบแล้ว / ไม่ใช่โปรแกรมตัดต่อในตัว+
ตัดเสร็จใน 5–10 นาทีทุกกรณียังพิสูจน์เป็นเกณฑ์ทั่วไปไม่ได้+
ถูกกว่าจ้างคน 10 เท่าต้องกำหนดต้นทุนให้เทียบกันได้+
หนึ่งไอเดีย สามรูปแบบคอนเทนต์
ปรับรูปแบบวิดีโอ แล้วดูกรอบภาพตัวอย่างและ Prompt เปลี่ยนตามแบบเรียลไทม์
ช่วยตัดต่อ?
พร้อมนำไปใช้กับ AI Agent
ตัวสร้าง Prompt นี้ไม่อัปโหลดคลิป ไม่เรียก API เสียเงิน และไม่ตัดต่อวิดีโอจริง ต้องใช้กับเครื่องมือที่ผู้ใช้อนุญาตภายหลัง
จำลองเวลาและต้นทุนของคุณเอง
เลื่อนตัวเลขเพื่อเปรียบเทียบสถานการณ์สมมติ ไม่ใช่ผล benchmark ที่ยืนยันแล้ว
ตัวเลขเป็นตัวอย่างเพื่อช่วยตัดสินใจ ไม่ใช่ผลประหยัดที่รับประกัน
AI ที่ตัดคลิปเป็น ไม่ได้ทำงานลำพัง
สิ่งที่น่าสนใจของ GPT-6 Astra และ GPT-6.1 Sol ไม่ได้อยู่ที่ว่าตัวโมเดลมีปุ่ม Render MP4 ซ่อนอยู่ แต่คือความสามารถในการจัดการงานหลายขั้นตอนร่วมกับเครื่องมืออื่น: อ่านไฟล์ วางลำดับเรื่อง ใช้แอป ตรวจผลลัพธ์ และแก้งานตามข้อเสนอแนะของมนุษย์
สำหรับคนทำคอนเทนต์ ความต่างสำคัญคือการเปลี่ยนจาก “ส่งคลิปให้ AI แล้วหวังว่าจะได้ผลงาน” ไปเป็น Workflow ที่ตรวจรับได้ทุกขั้น ตั้งแต่การเลือกช่วงเด็ด การสร้างซับ การตรวจคำผิด จนถึงการเรนเดอร์ไฟล์ตามสัดส่วนของแต่ละแพลตฟอร์ม
ข้อควรระวังคือระบบ Work ที่รันบนเว็บหรือมือถือไม่ได้หมายความว่า AI เข้าถึงคลิปบนคอมพิวเตอร์บ้านได้ทันที ต้องทำงานในสภาพแวดล้อมที่เปิดให้เข้าถึงไฟล์จริง มีเครื่องมือตัดต่อ และมีสิทธิ์ใช้งานอย่างชัดเจน
รวบรวมไฟล์ในโฟลเดอร์ที่เข้าถึงได้ ระบุสิทธิ์และข้อจำกัด
ให้ Agent เสนอ Hook, ช่วงตัด และโครงเรื่องก่อนตัดต่อ
ตรวจชื่อเฉพาะ ตัวเลข ภาษาไทย และความถูกต้องของคำกล่าวอ้าง
ตรวจความละเอียด เสียง ระยะเวลา safe zone และคำบรรยาย
การฝึกใน Lab: ตั้งโจทย์ “ทำ preview ของคลิปตัวอย่างหนึ่งรายการ” แล้วสังเกตว่าคำสั่งให้ worker รับ job_id, traceparent และขอบเขตการทำงานแยกกันหรือไม่ คัดลอก prompt มาเติมเกณฑ์รับงานเมื่อไม่มีหรือ context ผิดรูป จากนั้นเปรียบเทียบกับ brief หกส่วนด้านล่าง
3D Lab: วางแผนเส้นทาง trace บนงานวิดีโอ
มองขั้นตอนทำวิดีโอเป็นจุดต่อของงาน coordinator, queue, preview worker และ reviewer แล้วใช้ Lab สำรวจลำดับการส่งมอบ วาง annotation ว่าแต่ละช่วงสร้าง span หรือส่ง context ต่ออย่างไร กิจกรรมนี้ฝึกการวางแผนและระบุขอบเขตของการสังเกตการณ์เท่านั้น; Lab เป็น simulation ของวิดีโอ ไม่ได้อ่านหรือเขียน trace จริง ไม่ได้ตั้งค่า W3C header และไม่ได้แทน interactive แยกเฉพาะบทเรียนที่จะอยู่ถัดไป
เวลา/ต้นทุนเป็นสมมติฐาน ไม่ใช่ผลวัดจริง; หาก Lab ไม่พร้อม ให้อ่าน fallback ข้อความด้านบน
ทดลองส่ง Context ข้ามคิว
EDUCATIONAL SIMULATION: รหัสทั้งหมดเป็น fixture เพื่ออธิบายความสัมพันธ์ ไม่ใช่ trace จริง ไม่เรียก API หรือเก็บข้อมูล ค่าที่เปลี่ยนบนหน้านี้ไม่ใช่ผลทดสอบ Python หรือ OpenTelemetry
3D Context Carrier: เลือกจุดที่ข้อมูลต้องผ่าน
เลือกโหนดเพื่อดูว่าใครเขียนหรืออ่าน context จุดส่งงานกับจุดทำงานเป็นคนละช่วง แต่ต้องรักษาสายสัมพันธ์ของงาน
Coordinator สร้าง span ของตนเอง และ inject span_id นี้เป็น parent-id ใน traceparent ก่อนส่งงาน
อ่านแผนภาพแบบข้อความ
- Coordinator สร้าง trace_id และ span_id ประจำช่วงส่งงาน
- Envelope เก็บ business job_id แยกจาก traceparent
- Worker extract ค่า ตรวจ version 00 แล้วสร้าง span_id ใหม่ โดยอ้าง parent-id เดิม
- Log ของ worker เก็บ trace_id เดิมกับ span_id ใหม่ ไม่ใช้ IDs เหล่านี้อนุมัติ Tool
Visual Interactive แยก: ส่งมาแล้วต่อ Trace ได้หรือไม่?
ลองค่าปกติ เว้นว่าง หรือเปลี่ยน ID เป็นศูนย์ทั้งหมด แล้วดูผลนโยบายจำลอง เมื่อไม่รับ context จะเริ่ม trace ใหม่ ยังใช้ job_id ค้นงานได้ แต่สาย trace เดิมขาด แบบฝึกนี้ไม่มีการรันงานธุรกิจ
ยังไม่ได้จำลอง • ไม่มีการส่งข้อมูลออกจากหน้าเว็บ
ตัวตรวจสาธิตรับเฉพาะ version 00 ความยาวและ hex ตัวเล็กตามขอบเขตนี้ ไม่ใช่ W3C propagator เต็มรูปแบบ flags ที่รับมาถูกใช้เฉพาะ sampled bit ตาม baseline Level 1; การตรวจ fixture ว่า trace_id ตรงกับ job ที่เลือกเป็นเพียงการเทียบ mapping ที่ตั้งไว้ใน simulation ไม่ใช่กฎของ W3C ตัว worker จริงต้องตรวจ ownership ของงานแยกต่างหาก การเปลี่ยนเป็น trace ใหม่ไม่ใช่การรับรองสิทธิ์หรือความสำเร็จของงาน
ผลตัวอย่างเมื่อ JavaScript ใช้ไม่ได้
- JOB-A พร้อม header ปกติ: trace_id เดิม, span ของ worker ใหม่, parent_span_id เท่ากับ span ฝั่งส่ง
- JOB-B พร้อม header ประจำ B: fixture mapping ตรงกัน; ใส่ header ของ A แม้รูปแบบถูกต้อง simulation จะแจ้งว่าเป็น context ของอีกงาน
- header หายหรือผิดขอบเขต: trace ใหม่, parent_span_id ว่าง และ context_decision เป็น restarted
- IDs ตรงกันยังไม่ยืนยันว่า Tool สำเร็จ ต้องตรวจผลและสิทธิ์แยก
Task brief คัดลอกได้
เป้าหมาย: สร้าง preview storyboard สำหรับวิดีโอความยาว 30 วินาทีเรื่อง [หัวข้อ]
บริบท/ข้อมูล: รับเฉพาะ outline ที่ผ่านการอนุมัติ; ใช้ job_id=[รหัสงาน] และ request_id=[รหัสคำขอ] เป็น business identifiers
เครื่องมือ/สิทธิ์: ตัวอย่างนี้เป็น mock; ห้ามเรียก LLM, broker หรือเครื่องมือภายนอก
ขอบเขต: ส่ง traceparent รุ่น 00 ใน immutable envelope ของงานนี้เท่านั้น; ห้ามใช้ global current_trace; trace context ไม่ใช้อนุมัติสิทธิ์
สิ่งส่งมอบ/เกณฑ์ตรวจรับ: worker log มี job_id/request_id เดิม, trace_id ที่เชื่อมกับ coordinator, span_id ใหม่และ parent_span_id ของ coordinator; log ไม่มี payload/secret/PII; invalid context เริ่ม trace ใหม่พร้อม reason
จุดหยุด/ส่งต่อ: หยุดเมื่อ job validation ไม่ผ่านหรือข้อมูลไม่พอ; ส่งให้ผู้ดูแลตรวจสิทธิ์ตามระบบธุรกิจ; ไม่ retry หรือเผยแพร่จริงใน mock
brief นี้บอกเป้าหมาย ข้อมูล เครื่องมือ/สิทธิ์ ขอบเขต สิ่งส่งมอบ/เกณฑ์ และจุดหยุดครบ แต่ไม่ได้ทำให้ตัวอย่างกลายเป็น production pipeline
Acceptance criteria และ stopping condition
- Envelope ของแต่ละงานมี business IDs กับ traceparent แยกช่อง และเปลี่ยนค่าไม่ได้หลังสร้าง
- Parser รับเฉพาะ version 00 ที่รูปแบบครบและ ID ไม่เป็นศูนย์; flags อ่านได้ แต่ขาออก normalize reserved bits ตามรุ่น Level 1
- Worker สร้าง span ใหม่ และ parent_span_id ตรงกับ coordinator span ของ envelope นั้น
- เมื่อสลับลำดับงาน trace/job ยังจับคู่ถูก ไม่มี global current trace ที่แชร์
- เมื่อ context ขาดหรือผิดรูป ระบบจำลองเริ่ม trace ใหม่พร้อม reason; authorization/business validation แยกจาก trace parsing
- Log ไม่มี job payload หรือข้อมูลลับ และ output อธิบายว่าเป็น expected design ไม่ใช่ผลรัน
หยุด: เมื่อส่งต่อ context, สร้างความสัมพันธ์ parent-child, แยก identifiers และบันทึก event ตามเกณฑ์ครบใน mock แล้ว อย่าขยายเป็น broker, concurrency benchmark, OpenTelemetry exporter หรือ production deployment ในงานฝึกนี้
แบบฝึกหัด
เพิ่มกรณีที่ coordinator ส่ง envelope สองใบสลับลำดับ แล้วตรวจด้วยตาว่า worker log แต่ละใบอ้าง parent ของงานตัวเอง จากนั้นเพิ่ม malformed traceparent หนึ่งแบบและกรณีไม่มี context สังเกตว่า log มี restart reason โดยไม่ใส่ payload ลง log หากกำลังวางระบบ batch จริง ให้เขียนคำถามต่อว่าเหตุใด span link อาจเหมาะกว่า parent เดียว โดยยังไม่เปลี่ยน mock ให้รองรับ batch
ประเมินจากการแยก business ID กับ trace ID, การระบุขอบเขต parser v00 และการอธิบายว่า trace ไม่ authenticate job การตรวจ acceptance ของการออกแบบไม่ใช่ผลรันโค้ด ซึ่งยังคง NOT RUN
Quick Answer
ส่ง traceparent ใน envelope แยกต่อ job ให้ worker extract แล้วสร้าง span ใหม่ โดยเก็บ job_id/request_id แยกจาก trace_id/span_id และห้ามใช้ trace context เป็นสิทธิ์ ตัวอย่างนี้รองรับ traceparent v00 ใน in-process mock เท่านั้น
คำถามที่พบบ่อย
trace_id ใช้แทน job_id ได้ไหม?
ไม่ควร job_id เป็น business identity ที่ระบบใช้ค้นหาสถานะหรือ deduplicate งาน ส่วน trace_id ใช้รวมเหตุการณ์เชิงสังเกตการณ์ อายุและขอบเขตของสอง ID อาจไม่เท่ากัน
ทำไม worker ไม่ใช้ parent span id เป็น span ของตัวเอง?
เพราะ parent-id ใน context ที่ coordinator ส่งคือ span/request ฝั่ง caller; worker ต้องสร้าง span ใหม่เพื่อบอกช่วงทำงานของตน แล้วอ้าง parent ที่รับมา
traceparent ผิดรูปแล้วควรทิ้งงานหรือไม่?
ตัวอย่างเริ่ม trace ใหม่และบันทึกเหตุผล เพื่อไม่ให้ observability metadata ตัดสินชะตางานธุรกิจ แต่ระบบจริงต้องตรวจสิทธิ์และ job schema แยก หาก job ไม่ถูกต้องให้จัดการตาม business policy
ทำไมไม่ส่งค่า trace ใน global variable?
เมื่อมีงานสลับลำดับหรือทำพร้อมกัน ค่าที่แชร์อาจไม่ตรงกับ envelope ปัจจุบัน การส่ง context เป็น argument ของงานช่วยรักษาความสัมพันธ์เฉพาะงานในแบบจำลองนี้
ตัวอย่างนี้เป็น OpenTelemetry หรือไม่?
ไม่ใช่ เป็น JSON log กับ parser ขนาดเล็กจาก standard library เพื่อแสดงแนวคิด ยังไม่มี OTel SDK, propagator API, span processor, exporter หรือ backend
ถ้าหนึ่ง message มีหลายงานหรือ consumer หลายตัวล่ะ?
ความสัมพันธ์อาจไม่ใช่ parent-child เดียว OpenTelemetry messaging conventions ที่อ้างถึงยังมีสถานะ Development และกล่าวถึง span links ในกรณี batch/fan-out; ต้องออกแบบตาม semantics และ instrumentation ของ transport จริง
อ้างอิง
- W3C Trace Context Recommendation, โดยเฉพาะ traceparent และ trace-flags รุ่น 00
- OpenTelemetry: Context propagation และแนวทางความปลอดภัย
- OpenTelemetry Context specification: context immutability
- OpenTelemetry Logs Data Model: Trace Context Fields
- OpenTelemetry messaging spans (สถานะ Development ตามวันที่ตรวจ 2026-10-15)
- ไฟล์ตัวอย่างในบทเรียน:
code/trace_context_mock.pyและcode/test_trace_context_mock.py(AUTHOR MOCK; ไม่รัน)

ใส่ความเห็น