Python / discord.py
Install cloudline-bot, attach it to your discord.py bot, and the dashboard starts filling with telemetry on the next restart.
cloudline-bot is the official Python package for monitoring discord.py bots. Once attached, it sends a heartbeat (a small HTTP POST that says "I'm alive") every 30 seconds with gateway ping, RAM, CPU, slash command timing, and shard health.
You do not write the heartbeat loop or the metric collection yourself — the SDK reads them off your bot instance.
Install
pip install "cloudline-bot[metrics]"The [metrics] extra pulls in psutil, which is what unlocks CPU %, resident-set memory, and true process uptime. Without it the dashboard tiles for CPU / RAM / uptime stay blank — strongly recommended unless you're packaging for an unusual environment.
NOTE
Requires Python 3.9 or newer. discord.py is not installed by this package — your bot already has it.
Minimal setup
import os
import discord
from discord.ext import commands
from cloudline import attach
bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
attach(
bot,
bot_id="bot_abc123", # from your dashboard URL
secret=os.environ["CLOUDLINE_SECRET"], # the clb_live_… string
)
bot.run(os.environ["DISCORD_TOKEN"])That's the whole integration. attach() adds the listeners, and the heartbeat loop starts when discord.py emits on_ready.
IMPORTANT
attach() must run before bot.run() — bot.run() blocks until the bot is shut down, so anything after it never executes.
Auto-instrumentation requires commands.Bot or AutoShardedBot (anything with add_listener). A bare discord.Client works in manual mode (see below).
Options
| Argument | Default | What it does |
|---|---|---|
client | None | The bot to auto-instrument. Omit for manual mode. |
bot_id | — | Required. Your bot's ID from the dashboard. |
secret | — | Required. The clb_live_… heartbeat secret. |
interval | 30.0 | Heartbeat interval in seconds. Minimum 1.0. |
max_retries | 2 | How many times to retry one heartbeat on a transient failure (network drop / 5xx / 429). A bad secret (401) is never retried. |
base_url | https://cloudline.kescohhtwitch.workers.dev | Override only for self-hosted CloudLine. |
get_latency_ms / get_guilds | — | Manual metric providers if you're NOT passing a discord.py bot. |
Example with non-defaults:
attach(
bot,
bot_id="bot_abc123",
secret=os.environ["CLOUDLINE_SECRET"],
interval=10.0, # every 10s — needs Pro plan or higher
max_retries=3,
)What gets sent
Each heartbeat POSTs to /api/bots/{bot_id}/heartbeat with a JSON body. Every field is optional — anything the runtime can't measure is simply omitted.
- Connection:
latency_ms(gateway ping),gateway_ok,gateway_stale_sec - Guilds + shards:
guilds,shards_total,shards_connected,shard_detail - Slash + components:
slash_p50_ms,slash_p95_ms,slash_count,component_p50_ms,component_p95_ms,component_count,autocomplete_p50_ms,autocomplete_p95_ms,autocomplete_count - Process:
memory_mb,cpu_pct,uptime_sec,event_loop_lag_ms(asyncio loop lag) - Discord REST:
discord_rate_limit_hits(count of 429 responses; needs discord.py 2.4+) - Custom:
custom_metrics— see Custom metrics
p50 is the typical response time. p95 is the slow tail — 5% of interactions are slower than this number. Both reset and recalculate per heartbeat.
Auto-instrumented events
When you pass a commands.Bot, the SDK listens for:
on_ready→ starts the heartbeat loopon_app_command_completion→ records slash command end-to-end timingon_interaction→ records button / select-menu, slash autocomplete, and modal-submit timingon_error→ forwards exceptions to your Error Logon_rate_limit_hit→ counts Discord REST 429 responses (discord.py 2.4+ only — older versions just report 0)
Manual mode (no discord.py)
If you're using a different library or want full control:
import os, time
from cloudline import CloudLine
monitor = CloudLine(
bot_id="bot_abc123",
secret=os.environ["CLOUDLINE_SECRET"],
interval=30.0,
get_latency_ms=lambda: my_gateway_ping_ms(),
get_guilds=lambda: my_guild_count(),
)
# inside your running event loop:
monitor.start()
# feed command durations yourself:
t0 = time.monotonic()
await handle_command()
monitor.record_command((time.monotonic() - t0) * 1000)
# on shutdown:
monitor.stop()Available methods:
record_command(duration_ms)— record one slash / context menu / modal submitrecord_component(duration_ms)— record one button or select menu interactionrecord_autocomplete(duration_ms)— record one slash-autocomplete responserecord_rate_limit_hit()— note that Discord returned 429gauge(name, value)/counter(name, delta=1)— custom metrics, see Custom metricscapture_error(error, level="error", context=None)— push an error to the Error Logcapture_global_errors()— installsys.excepthook+ asyncio exception handler
Safety guarantees
The SDK is built so it cannot break your bot:
- Every HTTP request has a 10-second timeout. A hung connection cannot stall the bot.
- The heartbeat loop awaits each beat before sleeping, so two POSTs can never overlap.
- The error reporter has a per-minute cap (30 errors / 60 seconds) and a 1-minute dedupe on identical messages, so a crash loop cannot spam the API.
- A bad secret (401) stops retrying and just logs an error.
Common problems
- Status stays grey: you called
attach()afterbot.run()(unreachable code), or the env vars aren't loaded. See No data showing. - CPU / RAM tiles show
—: you installedcloudline-botwithout the[metrics]extra. Runpip install "cloudline-bot[metrics]". 401 — check bot_id/secretin the logs: you copied the wrong secret, or rotated it on the dashboard without redeploying.