Model Serving Monitor#

A live operations view for a simulated fraud-scoring model. Pick a traffic profile and go live to watch request rate, latency, error rate and feature drift respond. Threshold alerts fire and resolve automatically, and scaling replicas resolves a traffic surge. Click a recent request to inspect its payload.

All telemetry comes from a seeded pseudo-random simulator in this session. The dashboard starts paused.

import json
import math

from collections import deque

import numpy as np
import pandas as pd
import param

from bokeh.models import (
    ColumnDataSource, DatetimeTickFormatter, HoverTool, Span,
)
from bokeh.plotting import figure

import panel as pn

pn.extension("echarts", "tabulator", notifications=True, sizing_mode="stretch_width")

Configuration#

Traffic profiles, alert rules and the capacity model are plain data, so the simulator, the alert logic and the About tab all read from the same definitions.

SEED = 7
WINDOW = 600  # ticks of simulated history kept in memory; one tick is one simulated second
TREND_WINDOW = 120
TABLE_ROWS = 120
TREND_DIGITS = {"rps": 0, "p95": 0, "error_rate": 2, "drift": 3}
REPLICA_RPS = 90
START = pd.Timestamp("2026-09-29 12:00:00")

INDIGO, TEAL, AMBER, RED = "#6366F1", "#14B8A6", "#F59E0B", "#EF4444"

PROFILES = {
    "Steady": dict(rps=180, latency=38, errors=0.003, drift=0.04, blurb="Normal weekday traffic with healthy headroom."),
    "Traffic surge": dict(rps=560, latency=38, errors=0.003, drift=0.05, blurb="A marketing push triples demand. Add replicas to absorb it."),
    "Latency regression": dict(rps=190, latency=115, errors=0.004, drift=0.04, blurb="A slow feature-store lookup inflates every request."),
    "Data drift": dict(rps=200, latency=40, errors=0.006, drift=0.34, blurb="A new merchant segment shifts the input distribution."),
    "Upstream outage": dict(rps=175, latency=55, errors=0.07, drift=0.05, blurb="A dependency starts failing and requests return 5xx."),
}

RULES = [
    dict(key="p95", name="p95 latency above SLO", title="p95 latency > 250 ms", threshold=250, unit="ms", severity="warning", fmt="{:.0f}"),
    dict(key="error_rate", name="Error rate above budget", title="Error rate > 2 %", threshold=2.0, unit="%", severity="error", fmt="{:.2f}"),
    dict(key="drift", name="Feature drift detected", title="Drift PSI > 0.2", threshold=0.2, unit="PSI", severity="warning", fmt="{:.3f}"),
    dict(key="saturation", name="Replica saturation", title="Saturation > 90 %", threshold=90, unit="%", severity="info", fmt="{:.0f}"),
]
HEALTH_ICONS = {"Healthy": "check_circle", "Degraded": "warning", "Critical": "error"}
HEALTH_COLORS = {"Healthy": "success", "Degraded": "warning", "Critical": "error"}
FIRE_AFTER, RESOLVE_AFTER = 3, 5  # consecutive ticks, so a single noisy sample neither fires nor resolves an alert

ENDPOINTS = ["/v1/score", "/v1/score/batch", "/v1/explain"]
MERCHANTS = ["grocery", "travel", "electronics", "fuel", "restaurants", "gaming"]
COUNTRIES = ["DE", "FR", "NL", "ES", "IT", "PL"]

Simulator#

Each tick is one simulated second. Metrics ease towards the selected profile, and latency and errors grow once load exceeds the replicas’ capacity.

class ServingSimulator:
    """Deterministic pseudo-random telemetry for a model endpoint."""

    def __init__(self, seed=SEED):
        self.rng = np.random.default_rng(seed)
        self.tick = 0
        self.state = {k: v for k, v in PROFILES["Steady"].items() if k in ("rps", "latency", "errors", "drift")}
        self.request_id = 10_000

    def step(self, profile, replicas):
        rng, target = self.rng, PROFILES[profile]
        # Metrics ease towards the profile target so incidents ramp up and recover rather than switching instantly.
        for key, value in self.state.items():
            self.state[key] = value + (target[key] - value) * 0.08
        rps = max(self.state["rps"] * (1 + 0.07 * math.sin(self.tick / 40)) * rng.lognormal(0, 0.04), 1)
        util = rps / (replicas * REPLICA_RPS)
        queueing = 1 + 6 * max(0, util - 0.7) ** 2
        p50 = self.state["latency"] * queueing * rng.lognormal(0, 0.05)
        p95 = p50 * (2.3 + 0.35 * abs(rng.normal()))
        errors = self.state["errors"] + 0.2 * max(0, util - 0.95) + abs(rng.normal(0, 0.0008))
        drift = max(self.state["drift"] + rng.normal(0, 0.008), 0)
        metrics = dict(
            time=START + pd.Timedelta(seconds=self.tick), rps=rps, p50=p50, p95=p95,
            error_rate=min(errors, 1) * 100, drift=drift, saturation=min(util * 100, 100),
        )
        self.tick += 1
        return metrics, [self._request(metrics) for _ in range(rng.poisson(1.5) + 1)]

    def _request(self, metrics):
        rng = self.rng
        self.request_id += 1
        drift = metrics["drift"]
        err = metrics["error_rate"] / 100
        status = int(rng.choice([200, 500, 503], p=[1 - err, err * 0.6, err * 0.4]))
        latency = float(metrics["p50"] * rng.lognormal(0, 0.45))
        features = {
            "amount_eur": round(float(rng.lognormal(3.6 + 2.5 * drift, 0.8)), 2),
            "merchant_category": str(rng.choice(MERCHANTS[: 4 + int(drift > 0.15) * 2])),
            "country": str(rng.choice(COUNTRIES)),
            "card_present": bool(rng.random() > 0.35 + drift),
            "device_age_days": int(rng.gamma(2, 120)),
        }
        score = float(np.clip(rng.beta(1.2, 9) + drift * 0.6, 0, 1))
        decision = "decline" if score > 0.7 else "review" if score > 0.4 else "approve"
        payload = {
            "request_id": f"req-{self.request_id}",
            "model": "fraud-scorer",
            "model_version": "3.2.1",
            "endpoint": (endpoint := str(rng.choice(ENDPOINTS, p=[0.8, 0.15, 0.05]))),
            "features": features,
            "response": (
                {"status": status, "score": round(score, 4), "decision": decision} if status == 200
                else {"status": status, "error": "upstream feature store timeout"}
            ),
            "latency_ms": round(latency, 1),
        }
        row = dict(
            time=metrics["time"].strftime("%H:%M:%S"), request_id=payload["request_id"], endpoint=endpoint,
            status=status, latency_ms=round(latency), score=round(score, 3) if status == 200 else math.nan,
            decision=decision if status == 200 else "error",
        )
        return row, payload

Pure helpers#

Everything the dashboard displays is derived from parameter values by these functions. The panes bind to them once, so a tick only changes parameters and never rebuilds a component.

def availability(recent):
    """Request-weighted availability over a window of metric samples."""
    return round(100 - float((recent.error_rate * recent.rps).sum() / recent.rps.sum()), 2)


def health_for(active):
    severities = {record["rule"]["severity"] for record in active.values()}
    return "Critical" if "error" in severities else "Degraded" if severities - {"info"} else "Healthy"


def signal_severity(active, rule):
    return rule["severity"] if rule["key"] in active else "success"


def signal_variant(active, key):
    return "filled" if key in active else "outlined"


def signal_text(active, key):
    return f"Firing since {active[key]['started']:%H:%M:%S}" if key in active else "Within threshold"


def payload_summary(payload):
    if not payload:
        return "Click a request to inspect its payload."
    status = payload["response"]["status"]
    return f"**{payload['request_id']}** on `{payload['endpoint']}` returned **{status}** in **{payload['latency_ms']:.0f} ms**."


def payload_severity(payload):
    if not payload:
        return "info"
    return "success" if payload["response"]["status"] == 200 else "error"


def payload_json(payload):
    return json.dumps(payload, indent=2) if payload else ""


def profile_blurb(profile):
    return PROFILES[profile]["blurb"]


def profile_severity(profile):
    return "info" if profile == "Steady" else "warning"


def capacity_text(replicas):
    return f"≈ {replicas * REPLICA_RPS} req/s before queueing starts to hurt latency."


def status_style(status):
    return f"color: {RED}; font-weight: 600" if status >= 500 else ""


def history_items(alerts):
    # The number of alerts varies, so this is the one place that builds components; the Column holding it persists.
    if not alerts:
        return [pn.ui.Typography("No alerts have fired yet. Pick an incident profile and go live.", sx={"color": "text.secondary", "p": 2})]
    items = []
    for r in alerts:
        rule = r["rule"]
        resolved = r["resolved"] is not None
        ended = f"{r['resolved']:%H:%M:%S}" if resolved else "ongoing"
        duration = f"{(r['resolved'] - r['started']).seconds} s" if resolved else "active"
        peak = f"{rule['fmt'].format(r['peak'])} {rule['unit']}" if resolved else "pending"
        body = (
            f"**Threshold** {rule['threshold']} {rule['unit']} · **Peak** {peak} · "
            f"**Window** {r['started']:%H:%M:%S} to {ended} · **Severity** {rule['severity']}"
        )
        status = "✓" if resolved else "●"
        items.append((f"{status} {rule['name']} · {r['started']:%H:%M:%S} · {duration}", pn.ui.Markdown(body)))
    return [pn.ui.Accordion(*items, active=[0], title_variant="subtitle1", margin=0)]


def about_text(seed, interval):
    return f"""
All telemetry is generated in this session by a seeded pseudo-random simulator; nothing is collected or sent anywhere.

| Setting | Value |
|---|---|
| Seed | `{seed}` |
| Update interval | {interval} ms per simulated second (change in the sidebar) |
| History kept | {WINDOW} ticks ({WINDOW // 60} simulated minutes), older samples roll off |
| Capacity model | {REPLICA_RPS} req/s per replica, latency grows quadratically above 70% saturation |
| Alert rules | {", ".join(f"{r['name']} (> {r['threshold']} {r['unit']})" for r in RULES)} |
| Hysteresis | fire after {FIRE_AFTER} consecutive breaches, resolve after {RESOLVE_AFTER} clear ticks |

Try **Traffic surge**, watch saturation and latency climb, then drag **Replicas** up to 8 to recover.
"""

Building blocks#

Charts are Bokeh figures backed by a ColumnDataSource, so ticks stream single rows into them instead of re-rendering the plot.

def chart_card(title, y_label, lines, threshold=None, band=None):
    """Return a titled Paper holding a streaming time-series chart, and the source that feeds it."""
    source = ColumnDataSource(data={})
    fig = figure(
        height=250, x_axis_type="datetime", toolbar_location=None, y_axis_label=y_label,
        sizing_mode="stretch_width", min_border_left=50,
    )
    fig.xaxis.formatter = DatetimeTickFormatter(minutes="%H:%M", seconds="%H:%M:%S")
    fig.y_range.start = 0
    if band:
        fig.varea(x="time", y1=band[0], y2=band[1], source=source, color=lines[-1][2], alpha=0.12)
    renderers = [
        fig.line(x="time", y=field, source=source, color=color, line_width=2, legend_label=label)
        for field, label, color in lines
    ]
    if threshold is not None:
        fig.add_layout(Span(location=threshold, dimension="width", line_color=RED, line_dash="dashed", line_width=1.5))
    fig.add_tools(HoverTool(
        renderers=renderers[-1:], mode="vline",
        tooltips=[("Time", "@time{%H:%M:%S}"), *((label, f"@{field}{{0,0.[000]}}") for field, label, _ in lines)],
        formatters={"@time": "datetime"},
    ))
    legend = fig.legend[0]
    legend.orientation, legend.background_fill_alpha, legend.border_line_alpha = "horizontal", 0, 0
    legend.visible = len(lines) > 1
    fig.add_layout(legend, "above")
    fig.grid.grid_line_alpha = 0.4
    card = pn.ui.Paper(
        pn.ui.Typography(title, variant="subtitle1", sx={"fontWeight": 600, "px": 1}),
        pn.ui.Bokeh(fig, height=250), sx={"p": 1.5},
    )
    return card, source


def trend(label, color, plot_type, higher_is_worse=True):
    return pn.ui.Trend(
        label=label, plot_type=plot_type, plot_color=color, height=150, sizing_mode="stretch_width",
        pos_color=RED if higher_is_worse else TEAL, neg_color=TEAL if higher_is_worse else RED,
    )


def kpi_cell(obj, size):
    return pn.ui.Grid(pn.ui.Paper(obj, sx={"p": 1, "height": "100%"}, margin=0, height=170), size=size)

Dashboard#

The monitor holds all state as parameters. Every pane, tile, gauge and the payload dialog is built once in __init__ and bound to those parameters. The periodic callback is created once, paused, and toggled through its running parameter. Charts, trend indicators and the request table are the exception: they stream into bounded buffers imperatively. Alerts use hysteresis and notify only on state transitions.

class ModelServingMonitor(pn.viewable.Viewer):
    """Live view of request rate, latency, errors and drift for a simulated model endpoint."""

    running = param.Boolean(default=False, doc="Whether the simulation advances.")
    profile = param.Selector(default="Steady", objects=list(PROFILES), doc="Traffic profile that drives the simulation.")
    replicas = param.Integer(default=4, bounds=(1, 10), doc="Serving replicas; each absorbs roughly 90 req/s.")
    interval = param.Selector(default=500, objects={"Fast (250 ms)": 250, "Normal (500 ms)": 500, "Slow (1 s)": 1000}, doc="Update interval.")
    seed = param.Integer(default=SEED, doc="Random seed used on reset.")
    reset = param.Event(doc="Restart the simulation from the seed.")
    health = param.Selector(default="Healthy", objects=["Healthy", "Degraded", "Critical"])
    saturation = param.Number(default=0, bounds=(0, 100), doc="Latest replica saturation in percent.")
    availability = param.Number(default=100, bounds=(0, 100), doc="Request-weighted availability over the last two minutes.")
    active_alerts = param.Dict(default={}, doc="Firing alert records keyed by rule.")
    alerts = param.List(default=[], doc="Alert records, newest first.")
    inspected = param.Dict(default={}, doc="Payload of the request shown in the dialog.")
    dialog_open = param.Boolean(default=False, doc="Whether the payload dialog is visible.")

    def __init__(self, **params):
        super().__init__(**params)
        self._callback = pn.state.add_periodic_callback(self._tick, period=self.interval, start=False)
        self._build()
        self._restart()

    # Simulation lifecycle

    def _restart(self):
        self._sim = ServingSimulator(self.seed)
        self._metrics = deque(maxlen=WINDOW)
        self._payloads = {}
        self._streaks = {rule["key"]: 0 for rule in RULES}
        self._peaks = {}
        # Pre-roll five minutes of steady traffic so the paused dashboard has context instead of empty charts.
        rows = []
        for _ in range(300):
            rows += self._record(*self._sim.step("Steady", self.replicas))
        frame = pd.DataFrame(self._metrics)
        for source in self._sources.values():
            source.data = {column: frame[column].to_numpy() for column in frame.columns}
        tail = frame.tail(TREND_WINDOW)
        for key, indicator in self._trends.items():
            indicator.data = {"x": list(range(self._sim.tick - len(tail) + 1, self._sim.tick + 1)), "y": tail[key].round(TREND_DIGITS[key]).tolist()}
        self._set_requests(pd.DataFrame(rows[-TABLE_ROWS:]))
        self._update_kpis()
        self.param.update(alerts=[], active_alerts={}, health="Healthy", inspected={}, dialog_open=False)

    def _record(self, metrics, requests):
        self._metrics.append(metrics)
        for row, payload in requests:
            self._payloads[row["request_id"]] = payload
        return [row for row, _ in requests]

    def _set_requests(self, frame):
        self._table.value = frame.reset_index(drop=True)
        self._payloads = {rid: self._payloads[rid] for rid in frame.request_id}

    def _append_requests(self, rows):
        # Clicks report a row position, so rows are appended rather than replaced to keep positions stable between ticks.
        new = pd.DataFrame(rows)
        if len(self._table.value) + len(new) > 2 * TABLE_ROWS:
            self._set_requests(pd.concat([self._table.value.tail(TABLE_ROWS), new]))
        else:
            self._table.stream(new, follow=True)

    def _tick(self):
        rows = self._record(*self._sim.step(self.profile, self.replicas))
        metrics = self._metrics[-1]
        with pn.io.hold():
            for source in self._sources.values():
                source.stream({k: [v] for k, v in metrics.items()}, rollover=WINDOW)
            x = self._sim.tick
            for key, indicator in self._trends.items():
                indicator.stream({"x": [x], "y": [round(metrics[key], TREND_DIGITS[key])]}, rollover=TREND_WINDOW)
            self._update_kpis()
            self._append_requests(rows)
            notices = self._evaluate_alerts(metrics)
        # Notifications sent inside pn.io.hold() currently fail on the client with "reference isn't known".
        if pn.state.notifications:
            for severity, message in notices:
                getattr(pn.state.notifications, severity)(message, duration=5000)

    def _update_kpis(self):
        self.param.update(
            saturation=round(float(self._metrics[-1]["saturation"])),
            availability=availability(pd.DataFrame(self._metrics).tail(120)),
        )

    def _evaluate_alerts(self, metrics):
        # Records are replaced rather than mutated so that every change is a new parameter value the panes can react to.
        active, alerts = dict(self.active_alerts), list(self.alerts)
        notices, resolved = [], []
        for rule in RULES:
            key, value = rule["key"], float(metrics[rule["key"]])
            record = active.get(key)
            breached = value > rule["threshold"]
            self._streaks[key] = self._streaks[key] + 1 if breached == (record is None) else 0
            if record is not None:
                self._peaks[key] = max(self._peaks[key], value)
            if record is None and self._streaks[key] >= FIRE_AFTER:
                record = dict(rule=rule, started=metrics["time"], resolved=None, peak=value)
                active[key], self._peaks[key], self._streaks[key] = record, value, 0
                alerts.insert(0, record)
                notices.append((rule["severity"], f"{rule['name']}: {rule['fmt'].format(value)} {rule['unit']}"))
            elif record is not None and self._streaks[key] >= RESOLVE_AFTER:
                done = {**record, "resolved": metrics["time"], "peak": self._peaks.pop(key)}
                alerts = [done if r is record else r for r in alerts]
                del active[key]
                self._streaks[key] = 0
                resolved.append(rule["name"])
        if notices or resolved:
            self.param.update(active_alerts=active, alerts=alerts, health=health_for(active))
        if resolved:
            notices.append(("success", f"Resolved: {', '.join(resolved)}"))
        return notices

    @param.depends("running", watch=True)
    def _toggle_running(self):
        self._callback.running = self.running

    @param.depends("interval", watch=True)
    def _update_interval(self):
        self._callback.period = self.interval

    @param.depends("reset", watch=True)
    def _reset(self):
        self.param.update(running=False, profile="Steady", replicas=4)
        self._restart()
        if pn.state.notifications:
            pn.state.notifications.info(f"Simulation reset with seed {self.seed}.")

    def _inspect(self, event):
        row = self._table.value.iloc[event.row]
        self.param.update(inspected=self._payloads.get(row.request_id, {}), dialog_open=True)

    def _sync_dialog_open(self, event):
        # Binding `open` only flows param -> dialog; mirror client-side closes back so the next click reopens it.
        self.dialog_open = event.new

    # Views

    def _build(self):
        self._signals = [
            pn.ui.Alert(
                pn.bind(signal_text, self.param.active_alerts, rule["key"]), title=rule["title"],
                severity=pn.bind(signal_severity, self.param.active_alerts, rule),
                variant=pn.bind(signal_variant, self.param.active_alerts, rule["key"]), margin=0, min_height=88,
            )
            for rule in RULES
        ]
        gauge = pn.ui.Gauge(
            value=self.param.saturation, label="Saturation", format="{value}%", bounds=(0, 100), height=170, sizing_mode="stretch_width",
            colors=[(0.7, TEAL), (0.9, AMBER), (1, RED)], annulus_width=8, title_size=12,
            custom_opts={"pointer": {"width": 4}, "detail": {"fontSize": 20, "offsetCenter": [0, "60%"]}, "axisLabel": {"show": False}, "splitLine": {"length": 6}},
        )
        number = pn.ui.Number(
            value=self.param.availability, label="Availability (2 min)", format="{value:.2f}%", font_size="34pt", title_size="11pt",
            colors=[(99.0, RED), (99.5, AMBER), (100, TEAL)], sizing_mode="stretch_width", align="center",
        )
        self._trends = {
            "rps": trend("Throughput (req/s)", INDIGO, "area", higher_is_worse=False),
            "p95": trend("p95 latency (ms)", AMBER, "line"),
            "error_rate": trend("Error rate (%)", RED, "bar"),
            "drift": trend("Feature drift (PSI)", TEAL, "step"),
        }
        kpi_size = {"xs": 12, "sm": 6, "md": 4, "xl": 2}
        self._kpis = pn.ui.Grid(
            *(kpi_cell(indicator, kpi_size) for indicator in self._trends.values()),
            kpi_cell(gauge, kpi_size),
            kpi_cell(pn.ui.Column(
                pn.ui.VSpacer(), number,
                pn.ui.Typography("SLO target 99.5%", variant="caption", align="center", sx={"color": "text.secondary"}), pn.ui.VSpacer(),
            ), kpi_size),
            container=True, spacing=2,
        )
        charts = {
            "Latency": chart_card("Latency", "ms", [("p50", "p50", INDIGO), ("p95", "p95", AMBER)], threshold=250, band=("p50", "p95")),
            "Throughput": chart_card("Throughput", "req/s", [("rps", "requests / s", INDIGO)], band=(0, "rps")),
            "Error rate": chart_card("Error rate", "%", [("error_rate", "5xx share", RED)], threshold=2.0, band=(0, "error_rate")),
            "Feature drift": chart_card("Feature drift", "PSI", [("drift", "population stability index", TEAL)], threshold=0.2),
        }
        self._sources = {title: source for title, (_, source) in charts.items()}
        self._charts = pn.ui.Grid(
            *(pn.ui.Grid(card, size={"xs": 12, "lg": 6}) for card, _ in charts.values()),
            container=True, spacing=2,
        )
        self._table = pn.ui.Tabulator(
            pd.DataFrame(), show_index=False, height=360, layout="fit_columns", disabled=True, selectable=False,
            titles={
                "time": "Time", "request_id": "Request", "endpoint": "Endpoint", "status": "Status",
                "latency_ms": "Latency (ms)", "score": "Score", "decision": "Decision",
            },
            formatters={"latency_ms": {"type": "progress", "min": 0, "max": 1500, "legend": True, "color": [TEAL, AMBER, RED]}},
            stylesheets=[":host .tabulator-row { cursor: pointer; }"],
        )
        self._table.style.map(status_style, subset=["status"])
        self._table.on_click(self._inspect)
        self._dialog = pn.ui.Dialog(
            pn.ui.Alert(
                pn.bind(payload_summary, self.param.inspected), severity=pn.bind(payload_severity, self.param.inspected),
                margin=(0, 0, 10, 0),
            ),
            pn.ui.CodeEditor(
                value=pn.bind(payload_json, self.param.inspected), visible=self.param.inspected.rx.bool(),
                language="json", readonly=True, height=300,
            ),
            open=self.param.dialog_open, title="Request payload", show_close_button=True,
            close_on_click=True, width_option="sm",
        )
        self._dialog.param.watch(self._sync_dialog_open, "open")
        self._page = pn.ui.Page(
            title="Model Serving Monitor",
            header=[
                pn.ui.Chip(
                    label="fraud-scorer 3.2.1 · simulated", icon="memory", variant="outlined", color="default", size="small",
                    margin=(0, 10), sx={"color": "inherit", "borderColor": "rgba(255,255,255,0.5)"},
                ),
                pn.ui.Chip(
                    label=self.param.health, icon=pn.bind(HEALTH_ICONS.get, self.param.health),
                    color=pn.bind(HEALTH_COLORS.get, self.param.health), size="small", margin=(0, 10),
                ),
            ],
            sidebar=[self._sidebar()],
            main=[self._main()],
            main_width={"xs": "100%", "xl": 1680},
            theme_config={
                "light": {"palette": {"primary": {"main": INDIGO}, "background": {"default": "#F6F7FB"}}, "shape": {"borderRadius": 10}},
                "dark": {"palette": {"primary": {"main": "#818CF8"}, "background": {"default": "#0B1020", "paper": "#131A2E"}}, "shape": {"borderRadius": 10}},
            },
        )

    def _sidebar(self):
        running = self.param.running.rx
        return pn.ui.Column(
            pn.ui.Row(
                pn.ui.Toggle.from_param(
                    self.param.running, label=running.where("Pause", "Go live"), icon=running.where("pause", "play_arrow"),
                    color="primary", description="", sizing_mode="stretch_width",
                ),
                pn.ui.Button.from_param(self.param.reset, label="Reset", icon="restart_alt", variant="outlined", sizing_mode="stretch_width"),
            ),
            pn.ui.Divider(margin=(10, 10)),
            pn.ui.Typography("Traffic profile", variant="overline", margin=(0, 10)),
            pn.ui.RadioBoxGroup.from_param(self.param.profile, label="", sizing_mode="stretch_width"),
            pn.ui.Alert(
                pn.bind(profile_blurb, self.param.profile), title=self.param.profile,
                severity=pn.bind(profile_severity, self.param.profile), margin=(0, 10),
            ),
            pn.ui.Divider(margin=(10, 10)),
            pn.ui.Typography("Capacity", variant="overline", margin=(0, 10)),
            pn.ui.IntSlider.from_param(self.param.replicas, label="Replicas", sizing_mode="stretch_width", marks=True),
            pn.ui.Typography(
                pn.bind(capacity_text, self.param.replicas), variant="caption", margin=(0, 10), sx={"color": "text.secondary"},
            ),
            pn.ui.Select.from_param(self.param.interval, label="Update interval", sizing_mode="stretch_width"),
        )

    def _main(self):
        tabs = pn.ui.Tabs(
            ("Recent requests", pn.ui.Column(
                pn.ui.Typography("Click a row to inspect the request payload.", variant="caption", sx={"color": "text.secondary"}, margin=(10, 0)),
                self._table,
            )),
            ("Alert history", pn.ui.Column(objects=pn.bind(history_items, self.param.alerts), height=400, scroll="y-auto")),
            ("About", pn.ui.Markdown(pn.bind(about_text, self.param.seed, self.param.interval), margin=(10, 16))),
            margin=0,
        )
        return pn.ui.Column(
            pn.ui.Grid(*(pn.ui.Grid(tile, size={"xs": 12, "sm": 6, "lg": 3}) for tile in self._signals), container=True, spacing=2),
            self._kpis,
            self._charts,
            pn.ui.Paper(tabs, sx={"p": 1.5}, margin=0),
            self._dialog,
            styles={"gap": "16px"}, margin=(10, 0),
        )

    def __panel__(self):
        # The Page is built once in __init__ so the preview and the served app share one set of linked components.
        return self._page

Preview#

The same monitor instance is displayed inline here and served below.

app = ModelServingMonitor()
app
if pn.state.served:
    app.servable()