Web Components Observability Stack คืออะไร
Web Components เป็นมาตรฐาน W3C สำหรับสร้าง reusable UI components ด้วย Custom Elements, Shadow DOM และ HTML Templates ทำงานได้ทุก framework Observability Stack คือชุดเครื่องมือสำหรับ monitor ระบบครบ 3 pillars: Metrics, Logs และ Traces การรวม Web Components กับ Observability ช่วยให้ track performance ของ frontend components ได้ละเอียด วัด render time, user interactions, error rates และ Core Web Vitals ในระดับ component บทความนี้อธิบายวิธีสร้าง observability stack สำหรับ Web Components applications
Web Components Fundamentals
# web_components.py — Web Components overview
import json
class WebComponentsBasics:
STANDARDS = {
"custom_elements": {
"name": "Custom Elements",
"description": "สร้าง HTML tags ใหม่ — , , ",
"api": "customElements.define('my-element', MyElement)",
},
"shadow_dom": {
"name": "Shadow DOM",
"description": "Encapsulated DOM tree — styles + markup แยกจาก main document",
"api": "this.attachShadow({ mode: 'open' })",
},
"html_templates": {
"name": "HTML Templates",
"description": " + — reusable markup + content projection",
"api": "...",
},
}
EXAMPLE = """
// my-counter.js — Simple Web Component
class MyCounter extends HTMLElement {
constructor() {
super();
this.count = 0;
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.shadowRoot.querySelector('button')
.addEventListener('click', () => {
this.count++;
this.render();
// Emit custom event for observability
this.dispatchEvent(new CustomEvent('counter-changed', {
detail: { count: this.count },
bubbles: true,
composed: true,
}));
});
}
render() {
this.shadowRoot.innerHTML = `
Count:
`;
}
}
customElements.define('my-counter', MyCounter);
"""
def show_standards(self):
print("=== Web Components Standards ===\n")
for key, std in self.STANDARDS.items():
print(f"[{std['name']}]")
print(f" {std['description']}")
print()
def show_example(self):
print("=== Example Component ===")
print(self.EXAMPLE[:500])
wc = WebComponentsBasics()
wc.show_standards()
wc.show_example()
Observability Stack Architecture
# obs_stack.py — Observability stack for Web Components
import json
class ObservabilityStack:
PILLARS = {
"metrics": {
"name": "Metrics (ตัวเลข)",
"description": "ค่าตัวเลขที่วัดได้ — render time, error count, interaction rate",
"tools": ["Prometheus", "Grafana", "DataDog", "New Relic"],
"web_metrics": [
"Component render time (ms)",
"Core Web Vitals (LCP, FID, CLS)",
"Custom event frequency",
"Error rate per component",
"Memory usage per component",
],
},
"logs": {
"name": "Logs (ข้อความ)",
"description": "ข้อความบันทึกเหตุการณ์ — errors, warnings, user actions",
"tools": ["ELK Stack", "Loki + Grafana", "DataDog Logs", "Sentry"],
"web_logs": [
"Component lifecycle events",
"User interaction logs",
"Error stack traces",
"API call logs from components",
"Performance warnings",
],
},
"traces": {
"name": "Traces (การติดตาม)",
"description": "ติดตาม request flow — user click → component render → API call → response",
"tools": ["OpenTelemetry", "Jaeger", "Zipkin", "DataDog APM"],
"web_traces": [
"User interaction → component update chain",
"Component render → API fetch → DOM update",
"Navigation → route change → lazy load → render",
],
},
}
def show_pillars(self):
print("=== Observability Pillars ===\n")
for key, pillar in self.PILLARS.items():
print(f"[{pillar['name']}]")
print(f" {pillar['description']}")
print(f" Tools: {', '.join(pillar['tools'][:3])}")
print(f" Web metrics:")
for m in pillar.get('web_metrics', pillar.get('web_logs', pillar.get('web_traces', [])))[:3]:
print(f" • {m}")
print()
stack = ObservabilityStack()
stack.show_pillars()
Frontend Instrumentation
# instrumentation.py — Instrument Web Components
import json
class FrontendInstrumentation:
OTEL_CODE = """
// otel-web-components.js — OpenTelemetry for Web Components
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
// Setup OpenTelemetry
const provider = new WebTracerProvider({
resource: { attributes: { 'service.name': 'web-components-app' } },
});
provider.addSpanProcessor(new BatchSpanProcessor(
new OTLPTraceExporter({ url: 'https://otel-collector.example.com/v1/traces' })
));
provider.register({ contextManager: new ZoneContextManager() });
registerInstrumentations({
instrumentations: [getWebAutoInstrumentations({
'@opentelemetry/instrumentation-fetch': { enabled: true },
'@opentelemetry/instrumentation-xml-http-request': { enabled: true },
'@opentelemetry/instrumentation-document-load': { enabled: true },
'@opentelemetry/instrumentation-user-interaction': { enabled: true },
})],
});
// Custom Component Instrumentation
class ObservableElement extends HTMLElement {
constructor() {
super();
this._tracer = provider.getTracer('web-components');
}
trace(operationName, fn) {
const span = this._tracer.startSpan(operationName, {
attributes: {
'component.name': this.tagName.toLowerCase(),
'component.id': this.id || 'unknown',
},
});
try {
const result = fn();
span.setStatus({ code: 0 });
return result;
} catch (error) {
span.setStatus({ code: 2, message: error.message });
span.recordException(error);
throw error;
} finally {
span.end();
}
}
connectedCallback() {
this.trace('connectedCallback', () => {
this._renderStart = performance.now();
this.render();
const renderTime = performance.now() - this._renderStart;
// Report render time metric
performance.measure(`-render`, {
start: this._renderStart,
duration: renderTime,
});
});
}
}
"""
PERF_OBSERVER = """
// performance-observer.js — Monitor Web Component performance
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.includes('-render')) {
// Send to analytics
sendMetric({
component: entry.name.replace('-render', ''),
renderTime: entry.duration,
timestamp: Date.now(),
});
}
}
});
observer.observe({ entryTypes: ['measure'] });
// Core Web Vitals
import { onLCP, onFID, onCLS } from 'web-vitals';
onLCP((metric) => sendMetric({ name: 'LCP', value: metric.value }));
onFID((metric) => sendMetric({ name: 'FID', value: metric.value }));
onCLS((metric) => sendMetric({ name: 'CLS', value: metric.value }));
"""
def show_otel(self):
print("=== OpenTelemetry Setup ===")
print(self.OTEL_CODE[:600])
def show_perf(self):
print("\n=== Performance Observer ===")
print(self.PERF_OBSERVER[:400])
instr = FrontendInstrumentation()
instr.show_otel()
instr.show_perf()
Python Backend Collector
# collector.py — Backend metrics collector
import json
class MetricsCollector:
CODE = """
# frontend_collector.py — Collect frontend metrics
from fastapi import FastAPI, Request
from prometheus_client import Histogram, Counter, generate_latest
import json
from datetime import datetime
app = FastAPI()
# Prometheus metrics
RENDER_TIME = Histogram(
'web_component_render_seconds',
'Component render time',
['component'],
buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0]
)
INTERACTION_COUNT = Counter(
'web_component_interactions_total',
'User interactions',
['component', 'event_type']
)
ERROR_COUNT = Counter(
'web_component_errors_total',
'Component errors',
['component', 'error_type']
)
CWV_HISTOGRAM = Histogram(
'core_web_vitals',
'Core Web Vitals',
['metric_name'],
buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)
@app.post("/api/metrics")
async def collect_metrics(request: Request):
'''Collect frontend metrics'''
data = await request.json()
metric_type = data.get('type', '')
if metric_type == 'render':
RENDER_TIME.labels(
component=data.get('component', 'unknown')
).observe(data.get('duration', 0) / 1000) # ms to seconds
elif metric_type == 'interaction':
INTERACTION_COUNT.labels(
component=data.get('component', 'unknown'),
event_type=data.get('event', 'click')
).inc()
elif metric_type == 'error':
ERROR_COUNT.labels(
component=data.get('component', 'unknown'),
error_type=data.get('error_type', 'unknown')
).inc()
elif metric_type == 'cwv':
CWV_HISTOGRAM.labels(
metric_name=data.get('name', 'unknown')
).observe(data.get('value', 0) / 1000)
return {"status": "ok"}
@app.get("/metrics")
async def prometheus_metrics():
'''Prometheus scrape endpoint'''
return generate_latest()
# uvicorn frontend_collector:app --host 0.0.0.0 --port 9091
"""
def show_code(self):
print("=== Backend Collector ===")
print(self.CODE[:600])
collector = MetricsCollector()
collector.show_code()
Grafana Dashboard
# grafana.py — Grafana dashboard for Web Components
import json
import random
class GrafanaDashboard:
PANELS = {
"render_time": {
"title": "Component Render Time (P50, P95, P99)",
"query": "histogram_quantile(0.95, rate(web_component_render_seconds_bucket[5m]))",
"type": "Time series",
},
"cwv": {
"title": "Core Web Vitals",
"query": "histogram_quantile(0.75, rate(core_web_vitals_bucket[5m]))",
"type": "Stat panels (LCP, FID, CLS)",
},
"interactions": {
"title": "User Interactions per Component",
"query": "sum(rate(web_component_interactions_total[5m])) by (component)",
"type": "Bar chart",
},
"errors": {
"title": "Error Rate per Component",
"query": "sum(rate(web_component_errors_total[5m])) by (component, error_type)",
"type": "Time series + alerts",
},
"top_slow": {
"title": "Slowest Components",
"query": "topk(10, histogram_quantile(0.99, rate(web_component_render_seconds_bucket[5m])))",
"type": "Table",
},
}
def show_panels(self):
print("=== Grafana Panels ===\n")
for key, panel in self.PANELS.items():
print(f"[{panel['title']}]")
print(f" Query: {panel['query'][:60]}...")
print(f" Type: {panel['type']}")
print()
def sample_dashboard(self):
print("=== Live Dashboard ===")
print(f" LCP: {random.uniform(1.0, 3.5):.1f}s {'✅' if random.random() > 0.3 else '⚠️'}")
print(f" FID: {random.randint(10, 150)}ms {'✅' if random.random() > 0.3 else '⚠️'}")
print(f" CLS: {random.uniform(0.01, 0.25):.3f} {'✅' if random.random() > 0.3 else '⚠️'}")
components = ["data-table", "user-card", "search-bar", "nav-menu", "chart-widget"]
print(f"\n {'Component':<15} {'P95 Render':<12} {'Errors/min':<12} {'Interactions/min'}")
for c in components:
print(f" {c:<15} {random.randint(5, 200)}ms{'':<7} {random.randint(0, 5):<12} {random.randint(0, 100)}")
dash = GrafanaDashboard()
dash.show_panels()
dash.sample_dashboard()
FAQ - คำถามที่พบบ่อย
Q: Web Components ยังน่าใช้ในปี 2026 ไหม?
A: ใช่ — ทุก browser รองรับ Custom Elements v1, Shadow DOM v1 เต็มที่แล้ว ข้อดี: framework-agnostic, ใช้ร่วมกับ React/Vue/Angular ได้, standards-based ข้อเสีย: DX ไม่ดีเท่า React/Vue, ecosystem เล็กกว่า ใช้เมื่อ: ต้องการ reusable components ข้าม frameworks, design system, micro frontends Libraries: Lit (Google), Stencil.js, FAST (Microsoft) ช่วยให้ DX ดีขึ้น
Q: OpenTelemetry จำเป็นสำหรับ frontend ไหม?
A: แนะนำ — OTel เป็น standard สำหรับ observability ทั้ง backend + frontend ข้อดี: distributed tracing ตั้งแต่ user click → frontend → API → backend → DB ข้อดี: vendor-neutral — เปลี่ยน backend (Jaeger → DataDog) ไม่ต้องเปลี่ยน code ทางเลือก: Sentry (error tracking), DataDog RUM, New Relic Browser — ง่ายกว่าแต่ vendor lock-in
Q: Core Web Vitals สำคัญแค่ไหน?
A: สำคัญมาก — Google ใช้ CWV เป็น ranking factor สำหรับ SEO LCP (Largest Contentful Paint): < 2.5s = ดี, > 4s = แย่ FID (First Input Delay): < 100ms = ดี, > 300ms = แย่ CLS (Cumulative Layout Shift): < 0.1 = ดี, > 0.25 = แย่ Web Components ที่ render ช้า ส่งผลต่อ LCP และ FID โดยตรง
Q: Shadow DOM ทำให้ observability ยากขึ้นไหม?
A: เล็กน้อย — Shadow DOM encapsulate DOM tree ทำให้ traditional selectors ใช้ไม่ได้ แก้ไข: ใช้ composed: true ใน CustomEvent เพื่อ bubble ผ่าน Shadow DOM ใช้ Performance API (performance.measure) แทน DOM manipulation timing OpenTelemetry web SDK รองรับ Shadow DOM events ได้
