Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions bot/extensions/dashboard_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
from bot.grace import Grace
from bot.models.extensions.widget_state import WidgetState

SUMMARY = """
# Welcome!
### This dashboard contain various monitoring information about **Grace**, our awesome bot!
Hopefully, you will find something useful here.

- [Website](https://codesociety.xyz)
- [Discord](https://discord.gg/FzgwHD7Am3)
- [Source](https://github.com/Code-Society-Lab/grace)
"""


class DashboardCog(
Cog, name="Dashboard", description="Feeds bot telemetry to the live dashboard"
Expand All @@ -26,11 +36,10 @@ def __init__(self, bot: Grace):

def cog_load(self) -> None:
self._load_counts()
dachshund.emit("summary", value=SUMMARY)

self.jobs.append(
self.bot.scheduler.add_job(
self._report_latency, "cron", minute="*/1", timezone=self.timezone
)
self.bot.scheduler.add_job(self._report_latency, "interval", minutes=1)
)
self.jobs.append(
self.bot.scheduler.add_job(
Expand Down Expand Up @@ -87,22 +96,25 @@ def _reset_daily_count(self) -> None:
self._save_counts()

def _report_member_count(self) -> None:
dachshund.emit(
"member_count",
value=sum(guild.member_count or 0 for guild in self.bot.guilds),
)
guild_id = self.bot.app.config.get("client", "guild_id")

if guild := self.bot.get_guild(guild_id):
dachshund.emit("member_count", value=guild.member_count)

@Cog.listener()
async def on_ready(self) -> None:
self._report_member_count()
dachshund.emit("members")

@Cog.listener()
async def on_member_join(self, _member: Member) -> None:
self._report_member_count()
dachshund.emit("members")

@Cog.listener()
async def on_member_remove(self, _member: Member) -> None:
self._report_member_count()
dachshund.emit("members")

@Cog.listener()
async def on_message(self, _message: Message) -> None:
Expand Down
7 changes: 3 additions & 4 deletions bot/grace.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,14 @@ async def load_extensions(self):

async def setup_hook(self):
await super().setup_hook()
dashboard.init_dachshund()

self.metrics = await DashboardMetrics.load()
dashboard.init_dachshund()
dashboard.build_dashboard()
dashboard.start_dashboard()
dashboard.start_dashboard({"bot": self})

async def on_ready(self):
logger.info(f"{self.user.name}#{self.user.id} is online and ready to use!")

async def on_reload(self):
await super().on_reload()
dashboard.init_dachshund()
dashboard.start_dashboard()
78 changes: 7 additions & 71 deletions lib/dashboard/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import dachshund

from bot import app
from lib.dashboard.pages.activity import activity_page
from lib.dashboard.pages.overview import overview_page
from lib.dashboard.sqlmodel_storage import SQLModelStorage
from lib.dashboard.widgets.gauge import (
Gauge,
) # registers the "gauge" widget type on import

DACHSHUND_EVENT_STORE = app.config.get("dachshund", "event_store") or "dachshund.db"
DACHSHUND_EVENT_STORE = app.config.get("dachshund", "event_store", "dachshund.db")


def init_dachshund():
Expand All @@ -15,78 +14,15 @@ def init_dachshund():
dachshund.default(path=DACHSHUND_EVENT_STORE, storage=SQLModelStorage())


def start_dashboard():
def start_dashboard(context: dict):
dachshund.init(
"Grace",
description="Code Society's Discord Bot",
host="0.0.0.0",
context=context,
)


def build_dashboard() -> None:
# latency
dachshund.chart(
"latency",
type="timeseries",
title="Live Latency",
y=["check", "command"],
size=2,
)

dachshund.widget(
"average_latency_gauge",
type="gauge",
title="Latency Health",
source="average_latency",
min=0,
max=500,
warning_at=150,
critical_at=300,
unit="ms",
size=1,
)

# message
dachshund.widget(
"daily_message_rate",
type="number",
title="Today's Messages Rate",
size=1,
)

dachshund.chart(
"weekly_message_counts",
type="bar",
title="Messages / day",
x="date",
size=2,
persist=True,
)

dachshund.chart(
"on_command",
type="bar",
title="Calls Per Command",
x="name",
size=3,
persist=True,
)

dachshund.chart(
"command_error",
type="bar",
title="Errors Per Command",
x="name",
size=3,
persist=True,
)

# connection events
dachshund.chart(
"connection_event",
type="bar",
title="Connection Events",
x="type",
size=3,
persist=True,
)
for page in (overview_page, activity_page):
dachshund.include(page)
Empty file added lib/dashboard/pages/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions lib/dashboard/pages/activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dachshund import BarChart, Number, Page

activity_page = Page("activity", title="Activity")

activity_page.row(
Number("daily_message_rate", title="Today's Messages Rate"),
BarChart("weekly_message_counts", title="Messages / day", x="date", persist=True),
widths=(1, 2),
)
activity_page.row(
BarChart("on_command", title="Calls Per Command", x="name", persist=True)
)
activity_page.row(
BarChart("command_error", title="Errors Per Command", x="name", persist=True)
)
57 changes: 57 additions & 0 deletions lib/dashboard/pages/overview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from dachshund import Number, Page, Table, Text, TimeSeriesChart, BarChart

from bot import app
from lib.dashboard.widgets.gauge import Gauge

overview_page = Page("overview", title="Overview")


def load_members(ctx: dict, cursor: str | None, page_size: int):
bot = ctx["bot"]
members = []

if guild := bot.get_guild(app.config.get("client", "guild_id")):
for member in guild.members:
members.append(
{
"id": member.id,
"name": member.name,
"nickname": member.nick,
"joined at": member.joined_at.strftime("%a %d %b %Y"),
}
)

start = int(cursor) if cursor else 0
rows = members[start : start + page_size]
next_cursor = str(start + page_size) if start + page_size < len(members) else None

return rows, next_cursor


overview_page.row(Text("summary"))
overview_page.row(
Gauge(
"average_latency_gauge",
title="Latency Health",
source="average_latency",
min=0,
max=500,
warning_at=150,
critical_at=300,
unit="ms",
),
TimeSeriesChart("latency", title="Live Latency", y=["check", "command"]),
widths=(1, 2),
)
overview_page.row(
Number("member_count"),
Table(
"members",
columns=["id", "name", "nickname", "joined at"],
loader=load_members,
),
widths=(1, 2),
)
overview_page.row(
BarChart("connection_event", title="Connection Events", x="type", persist=True)
)
2 changes: 1 addition & 1 deletion lib/dashboard/widgets/gauge.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Gauge(Widget):
## Example

```python
dachshund.widget("cpu", type="gauge", title="CPU Load", max=100, unit="%")
dachshund.row(Gauge("cpu", title="CPU Load", max=100, unit="%"))
dachshund.emit("cpu", value=42)
```
"""
Expand Down
61 changes: 45 additions & 16 deletions tests/extensions/test_dashboard_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,12 @@ def test_reset_daily_count__expect_persists_zeroed_count(dashboard_cog):


@patch("bot.extensions.dashboard_cog.dachshund.emit")
def test_report_member_count__expect_emits_summed_member_count(
def test_report_member_count__expect_emits_member_count_for_configured_guild(
mock_emit, dashboard_cog
):
"""Verify _report_member_count sums member_count across every guild."""
dashboard_cog.bot.guilds = [
type("Guild", (), {"member_count": 10})(),
type("Guild", (), {"member_count": 5})(),
]
"""Verify _report_member_count emits the member count for the configured guild."""
guild = type("Guild", (), {"member_count": 15})()
dashboard_cog.bot.get_guild = lambda guild_id: guild

dashboard_cog._report_member_count()

Expand All @@ -58,33 +56,65 @@ def test_report_member_count__expect_emits_summed_member_count(
@patch("bot.extensions.dashboard_cog.dachshund.emit")
async def test_on_ready__expect_reports_member_count(mock_emit, dashboard_cog):
"""Verify on_ready reports the current member count."""
dashboard_cog.bot.guilds = [type("Guild", (), {"member_count": 7})()]
guild = type("Guild", (), {"member_count": 7})()
dashboard_cog.bot.get_guild = lambda guild_id: guild

await dashboard_cog.on_ready()

mock_emit.assert_called_once_with("member_count", value=7)
mock_emit.assert_any_call("member_count", value=7)


@pytest.mark.asyncio
@patch("bot.extensions.dashboard_cog.dachshund.emit")
async def test_on_ready__expect_a_members_refresh_ping(mock_emit, dashboard_cog):
"""Verify on_ready pings the members table to refetch its loader."""
await dashboard_cog.on_ready()

mock_emit.assert_any_call("members")


@pytest.mark.asyncio
@patch("bot.extensions.dashboard_cog.dachshund.emit")
async def test_on_member_join__expect_reports_member_count(mock_emit, dashboard_cog):
"""Verify on_member_join reports the current member count."""
dashboard_cog.bot.guilds = [type("Guild", (), {"member_count": 8})()]
guild = type("Guild", (), {"member_count": 8})()
dashboard_cog.bot.get_guild = lambda guild_id: guild

await dashboard_cog.on_member_join(object())

mock_emit.assert_called_once_with("member_count", value=8)
mock_emit.assert_any_call("member_count", value=8)


@pytest.mark.asyncio
@patch("bot.extensions.dashboard_cog.dachshund.emit")
async def test_on_member_join__expect_a_members_refresh_ping(mock_emit, dashboard_cog):
"""Verify on_member_join pings the members table to refetch its loader."""
await dashboard_cog.on_member_join(object())

mock_emit.assert_any_call("members")


@pytest.mark.asyncio
@patch("bot.extensions.dashboard_cog.dachshund.emit")
async def test_on_member_remove__expect_reports_member_count(mock_emit, dashboard_cog):
"""Verify on_member_remove reports the current member count."""
dashboard_cog.bot.guilds = [type("Guild", (), {"member_count": 6})()]
guild = type("Guild", (), {"member_count": 6})()
dashboard_cog.bot.get_guild = lambda guild_id: guild

await dashboard_cog.on_member_remove(object())

mock_emit.assert_called_once_with("member_count", value=6)
mock_emit.assert_any_call("member_count", value=6)


@pytest.mark.asyncio
@patch("bot.extensions.dashboard_cog.dachshund.emit")
async def test_on_member_remove__expect_a_members_refresh_ping(
mock_emit, dashboard_cog
):
"""Verify on_member_remove pings the members table to refetch its loader."""
await dashboard_cog.on_member_remove(object())

mock_emit.assert_any_call("members")


@pytest.mark.asyncio
Expand Down Expand Up @@ -233,14 +263,13 @@ def test_load_counts__expect_emits_restored_counts(mock_emit, dashboard_cog):


def test_cog_load__expect_schedules_cron_jobs(dashboard_cog):
"""Verify cog_load schedules latency, count-persistence, and day-reset jobs, pinned to the cog's timezone."""
"""Verify cog_load schedules the latency, count-persistence and day-reset jobs."""
dashboard_cog.cog_load()

dashboard_cog.bot.scheduler.add_job.assert_any_call(
dashboard_cog._report_latency,
"cron",
minute="*/1",
timezone=dashboard_cog.timezone,
"interval",
minutes=1,
)
dashboard_cog.bot.scheduler.add_job.assert_any_call(
dashboard_cog._save_counts,
Expand Down
Empty file.
Loading
Loading