Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7023899
Add synapse_admin_user_count metric.
Half-Shot Jun 11, 2026
c89519e
typo
Half-Shot Jun 11, 2026
b924048
spacing
Half-Shot Jun 11, 2026
3c0e6f4
Potential fix for pull request finding
Half-Shot Jun 11, 2026
4335e1f
Potential fix for pull request finding
Half-Shot Jun 11, 2026
39468bc
Potential fix for pull request finding
Half-Shot Jun 11, 2026
170d120
Potential fix for pull request finding
Half-Shot Jun 11, 2026
a0dc0c3
Potential fix for pull request finding
Half-Shot Jun 11, 2026
2c1a9f3
test update
Half-Shot Jun 11, 2026
e420c3b
lint
Half-Shot Jun 11, 2026
fb2ec7d
Bump metrics dependency to support .clear()
Half-Shot Jun 11, 2026
715c253
update lock
Half-Shot Jun 11, 2026
772082e
Active is fine
Half-Shot Jun 15, 2026
904385c
Review changes
Half-Shot Jun 16, 2026
1b9ce6c
update tests
Half-Shot Jun 16, 2026
3ae1139
Update tests
Half-Shot Jun 16, 2026
742d92f
Merge remote-tracking branch 'origin/develop' into hs/metric-for-all-…
Half-Shot Jun 16, 2026
d821bbe
update lock
Half-Shot Jun 16, 2026
b240e9c
More tidyup
Half-Shot Jun 18, 2026
a9b58dd
Add a comment
Half-Shot Jun 23, 2026
33b10c6
Update synapse/app/phone_stats_home.py
Half-Shot Jun 25, 2026
4176100
Update tests/metrics/test_phone_home_stats.py
Half-Shot Jun 25, 2026
7892274
Apply suggestions from code review
Half-Shot Jun 25, 2026
539a0c9
comment COUNT_USERS_INTERVAL value source.
Half-Shot Jun 25, 2026
54d6b58
Merge remote-tracking branch 'origin/develop' into hs/metric-for-all-…
Half-Shot Jun 25, 2026
49ed33e
use poetry 2.4.1 for lock
Half-Shot Jun 25, 2026
4f40339
lint
Half-Shot Jun 25, 2026
c3003e1
Use more standard import
MadLittleMods Jun 25, 2026
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
1 change: 1 addition & 0 deletions changelog.d/19848.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add new metric `synapse_admin_user_count` which tracks how many users are registered to the homeserver.
Comment thread
Copilot marked this conversation as resolved.
Outdated
24 changes: 24 additions & 0 deletions synapse/app/phone_stats_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@
"Registered users with reserved threepids",
labelnames=[SERVER_NAME_LABEL],
)
user_count_gauge = Gauge(
Comment thread
Half-Shot marked this conversation as resolved.
"synapse_admin_user_count",
Comment thread
Half-Shot marked this conversation as resolved.
Outdated
Comment thread
Half-Shot marked this conversation as resolved.
Outdated
"Total user count within the Synapse database",
labelnames=["app_service", SERVER_NAME_LABEL],
)
Comment thread
Copilot marked this conversation as resolved.
Comment thread
Half-Shot marked this conversation as resolved.


def phone_stats_home(
Expand Down Expand Up @@ -261,6 +266,25 @@ async def _generate_monthly_active_users() -> None:
_generate_monthly_active_users,
)

def generate_total_users() -> "defer.Deferred[None]":
async def _generate_total_users() -> None:
store = hs.get_datastores().main

result = await store.get_user_count_by_service()

for app_service, count in result:
user_count_gauge.labels(
app_service=app_service, **{SERVER_NAME_LABEL: server_name}
).set(float(count))

Comment thread
Copilot marked this conversation as resolved.
return hs.run_as_background_process(
"generate_total_users",
_generate_total_users,
)

generate_total_users()
Comment thread
Half-Shot marked this conversation as resolved.
Outdated
clock.looping_call(generate_total_users, Duration(minutes=5))
Comment thread
Copilot marked this conversation as resolved.
Outdated

if hs.config.server.limit_usage_by_mau or hs.config.server.mau_stats_only:
generate_monthly_active_users()
clock.looping_call(generate_monthly_active_users, Duration(minutes=5))
Expand Down
26 changes: 26 additions & 0 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,32 @@ def _count_users(txn: LoggingTransaction) -> int:

return await self.db_pool.runInteraction("count_real_users", _count_users)

async def get_user_count_by_service(self) -> list[tuple[str, int]]:
"""Counts users grouped by their appservice.

Returns:
A list of tuples (appservice_id, count). "native" is emitted as the
appservice for users that don't come from appservices (i.e. native Matrix
users).
Comment thread
MadLittleMods marked this conversation as resolved.

"""

def _get_user_count_by_service(
txn: LoggingTransaction,
) -> list[tuple[str, int]]:
sql = """
SELECT COALESCE(appservice_id, 'native'), COUNT(name)
FROM users WHERE deactivated = 0
GROUP BY appservice_id;
"""
Comment thread
Copilot marked this conversation as resolved.
Outdated

txn.execute(sql)
return cast(list[tuple[str, int]], txn.fetchall())

return await self.db_pool.runInteraction(
"get_user_count_by_service", _get_user_count_by_service
)

async def generate_user_id(self) -> str:
"""Generate a suitable localpart for a guest user

Expand Down
30 changes: 30 additions & 0 deletions tests/metrics/test_phone_home_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
from synapse.app.phone_stats_home import (
PHONE_HOME_INTERVAL,
start_phone_stats_home,
user_count_gauge,
)
from synapse.rest import admin, login, register, room
from synapse.server import HomeServer
from synapse.types import JsonDict
from synapse.util.clock import Clock
from synapse.util.duration import Duration

from tests import unittest
from tests.server import ThreadedMemoryReactorClock
Expand Down Expand Up @@ -261,3 +263,31 @@ def test_phone_home_stats(self) -> None:
synapse_logger = logging.getLogger("synapse")
log_level = synapse_logger.getEffectiveLevel()
self.assertEqual(phone_home_stats["log_level"], logging.getLevelName(log_level))

def test_generate_total_users_gauge(self) -> None:
"""
Test that generate_total_users() populates user_count_gauge correctly,
counting only active (non-deactivated) users split by app_service.
Comment thread
Half-Shot marked this conversation as resolved.
Outdated
"""
server_name = self.hs.config.server.server_name

# Register two native users and deactivate one to confirm it is excluded.
self.register_user("gauge_user_1", "password")
user_2 = self.register_user("gauge_user_2", "password")
self.get_success(
self.store.set_user_deactivated_status(user_id=user_2, deactivated=True)
)
Comment thread
Half-Shot marked this conversation as resolved.
Outdated

# Advance past the 5-minute looping interval so generate_total_users fires again.
self.reactor.advance(Duration(minutes=5).as_secs() + 1)
Comment thread
Half-Shot marked this conversation as resolved.
Outdated
Comment thread
Half-Shot marked this conversation as resolved.
Outdated

# Collect gauge samples for our server_name.
samples = {
sample.labels["app_service"]: sample.value
for metric_family in user_count_gauge.collect()
for sample in metric_family.samples
if sample.labels.get("server_name") == server_name
Comment thread
Half-Shot marked this conversation as resolved.
Outdated
}
Comment thread
Half-Shot marked this conversation as resolved.

# Only the one active native user should be counted.
self.assertEqual(samples.get("native"), 1.0)
Loading