Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 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_non_deactivated_user_count` which tracks the number of non-deactivated users in the database, split by `app_service`.
52 changes: 26 additions & 26 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ dependencies = [
"msgpack>=0.5.2",
"phonenumbers>=8.2.0",
# we use GaugeHistogramMetric, which was added in prom-client 0.4.0.
# `prometheus_client.metrics` was added in 0.5.0, so we require that too.
# We chose 0.6.0 as that is the current version in Debian Buster (oldstable).
"prometheus-client>=0.6.0",
# `Gauge.clear()` was added in 0.10.0.
"prometheus-client>=0.10.0",
# we use `order`, which arrived in attrs 19.2.0.
# Note: 21.1.0 broke `/sync`, see https://github.com/matrix-org/synapse/issues/9936
"attrs>=19.2.0,!=21.1.0",
Expand Down
44 changes: 43 additions & 1 deletion synapse/app/phone_stats_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,21 @@
Phone home stats are sent every 3 hours
"""

COUNT_USERS_INTERVAL = Duration(minutes=5)
"""
We recalculate synapse_non_deactivated_user_count every 5 minutes, which allows
for a reasonable level of accuracy without consuming too much database time.
"""

# Contains the list of processes we will be monitoring
# currently either 0 or 1
_stats_process: list[tuple[int, "resource.struct_rusage"]] = []

# FIXME: These gauges should probably be moved somewhere else as they are NOT included
# in the phone home stats payload. It appears that they were historically organized here
# during a refactor to ensure that we only calculate them on the workers designated to
# `hs.config.run_background_tasks` and because they are metrics.
#
# Gauges to expose monthly active user control metrics
current_mau_gauge = Gauge(
"synapse_admin_mau_current",
Expand All @@ -73,6 +84,11 @@
"Registered users with reserved threepids",
labelnames=[SERVER_NAME_LABEL],
)
user_count_gauge = Gauge(
Comment thread
Half-Shot marked this conversation as resolved.
"synapse_non_deactivated_user_count",
"Total non-deactivated user count within the Synapse database, split by appservice",
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 @@ -263,9 +279,35 @@ async def _generate_monthly_active_users() -> None:

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))
clock.looping_call(generate_monthly_active_users, COUNT_USERS_INTERVAL)
# End of monthly active user settings

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

result = await store.get_user_count_by_service()

# Should an appservice disappear from the results (because all of the users
# were deleted/deactivated), we want to ensure we don't leave behind any
# stale data.
user_count_gauge.clear()
Comment thread
Half-Shot marked this conversation as resolved.

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,
)

if hs.config.metrics.enable_metrics:
generate_non_deactivated_user_count()
clock.looping_call(generate_non_deactivated_user_count, Duration(minutes=5))

if hs.config.metrics.report_stats:
logger.info("Scheduling stats reporting for 3 hour intervals")
clock.looping_call(
Expand Down
27 changes: 27 additions & 0 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,33 @@ 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(NULLIF(appservice_id, ''), 'native') AS app_service, COUNT(*) AS count
FROM users
WHERE deactivated = 0
GROUP BY COALESCE(NULLIF(appservice_id, ''), 'native')
Comment thread
MadLittleMods marked this conversation as resolved.
Comment thread
Half-Shot marked this conversation as resolved.
"""

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
40 changes: 40 additions & 0 deletions tests/metrics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.

from synapse.metrics import (
Comment thread
Half-Shot marked this conversation as resolved.
REGISTRY,
generate_latest,
)


def get_latest_metrics() -> dict[str, str]:
Comment thread
Half-Shot marked this conversation as resolved.
"""
Collect the latest metrics from the registry and parse them into an easy to use map.
The key includes the metric name and labels.

Example output:
{
"synapse_util_caches_cache_size": "0.0",
"synapse_util_caches_cache_max_size{name="some_cache",server_name="hs1"}": "777.0",
...
}
"""
metric_map = {
x.split(b" ")[0].decode("ascii"): x.split(b" ")[1].decode("ascii")
for x in filter(
lambda x: len(x) > 0 and not x.startswith(b"#"),
generate_latest(REGISTRY).split(b"\n"),
)
}

return metric_map
Loading
Loading