guide · 5 min read

Getting started — from zero to a first alert.

A working snitchbot install takes three inputs: a Telegram bot token, a chat id to write to, and a single init() call. Uncaught exceptions are captured automatically once init() has run; everything else (notify(), @watch_slow, request_context) is opt-in on top of that.

Works withNeedsSince
Python >= 3.10snitchbot0.1.0

Step 01 — step :00 · create the bot

Open a chat with @BotFather, send /newbot, pick a display name and a username. BotFather returns an HTTP token that looks like 123456789:AA.... Keep it secret — anyone with this token can post as your bot.

$ # in Telegram, DM @BotFather
$ /newbot
$ # follow prompts; copy the token it returns

Then add the bot to the chat you want alerts in. For a DM, search for its username and send /start. For a group, add the bot as a member — it does not need admin rights.

Step 02 — step :02 · find the chat id

snitchbot writes to a numeric chat id, not a username. The easiest way to find it is @userinfobot.

  • DM target: start a chat with @userinfobot, it replies with your id.
  • Group target: add @userinfobot to the group, it posts the group id (negative integer, e.g. -1001234567890). Remove it afterwards.

Put both values into a local .env file — snitchbot reads it on init():

# .env
SNITCHBOT_TOKEN=123456789:AA...
SNITCHBOT_CHAT_ID=-1001234567890

💡 Tip: you can also pass token= and chat_id= directly to init(). The env-file path is just the cheapest way to keep secrets out of source.

Step 03 — step :05 · install and call init

Install the package and call init() as early as you can — before any other import that might crash.

$ uv add snitchbot
# pip install snitchbot also works
# main.py
import snitchbot

snitchbot.init("my-service")

# That's it. Uncaught exceptions are now reported.
# Nothing below is required — it's there to show what else you can do.

snitchbot.notify("deploy complete", severity="warning")

@snitchbot.watch_slow(threshold_ms=1000)
async def process_order(order_id: str) -> None:
    ...

with snitchbot.request_context(trace_id="req-abc", user_id="42"):
    snitchbot.notify("checkout started")

Run the file. The startup lifecycle event lands in Telegram within a second:

▶ my-service started
━━━━━━━━━━━━━━━━━━
pid        1718711
time       15:12:04 UTC

Now trigger a crash to confirm the excepthook is wired:

# crash_test.py
import snitchbot

snitchbot.init("my-service")

raise RuntimeError("sanity check")
$ python crash_test.py

📨 Telegram shows:

🔴 crash · my-service · a1b2c3
RuntimeError: sanity check
Details
  time     15:12:05 UTC
  pid      1718711
Stack (top 3 user frames)
  crash_test.py:5 in <module>()
    raise RuntimeError("sanity check")

If you see that message, you are done. Everything else in the docs is optional polish on top of this foundation.

Two modes

snitchbot picks one of two operating modes by inspecting the chat at startup. Nothing to configure — the right mode is detected automatically.

Simple modeForum mode
Chat typeDM, group, channelSupergroup with Topics enabled
Services per botone (or many sharing one stream)many (one topic each)
/status scopeglobalscoped to current topic; global in General
Setup overheadnoneenable Topics + give bot Manage Topics right

Want one bot to host alerts for many services in clean per-service threads? See Forum mode.

Troubleshooting

Q: init() ran, but no alert arrived after a crash. A: Check the sidecar is alive — ps -ef | grep snitchbot.sidecar. If it isn’t, look at stderr of your program. The most common cause is a bad token (401 Unauthorized from the Bot API) or a chat id the bot was never added to. snitchbot never raises these — they are logged at WARNING.

Q: I see snitchbot vitals cache: 60 samples × 5s = 300s history, ~7 KB in my logs. Is that an error? A: No. It’s a one-line info log from init() showing the sampling buffer size. Set sample_interval_sec= or tune AnomalyConfig if you want a different cadence.

Q: How do I disable snitchbot in tests without editing code? A: Set SNITCHBOT_DISABLED=1 in the environment. init() returns immediately and no sidecar is spawned. You can also pass disabled=True explicitly.

Q: Gunicorn / Uvicorn workers — do I get one alert per worker? A: Yes, each worker has its own sidecar and posts its own lifecycle events. A post-fork hook reinitialises in every child. Use the role= kwarg (e.g. "web", "worker") to distinguish them in the chat.

What’s next