Repository: jupyterhub/jupyterhub

Issue:
Migrate from jupyter_telemetry to jupyter_events?
The `jupyter_telemetry` package is [said to be unmaintained](https://github.com/jupyter/telemetry/pull/69) and it test suite does not work with Python 3.12. It does not seem to be used heavily in JupyterHub, can you migrate to [jupyter_events](https://github.com/jupyter/jupyter_events) instead?


Discussion:
Yes, we should do that. Thanks for bringing it up!

Resolving patch:
diff --git a/docs/source/conf.py b/docs/source/conf.py
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -289,6 +289,7 @@ def setup(app):
     r"https://github.com/[^/]*$",  # too many github usernames / searches in changelog
     "https://github.com/jupyterhub/jupyterhub/pull/",  # too many PRs in changelog
     "https://github.com/jupyterhub/jupyterhub/compare/",  # too many comparisons in changelog
+    "https://schema.jupyter.org/jupyterhub/.*",  # schemas are not published yet
     r"https?://(localhost|127.0.0.1).*",  # ignore localhost references in auto-links
     r"https://linux.die.net/.*",  # linux.die.net seems to block requests from CI with 403 sometimes
     # don't check links to unpublished advisories
diff --git a/jupyterhub/app.py b/jupyterhub/app.py
--- a/jupyterhub/app.py
+++ b/jupyterhub/app.py
@@ -21,6 +21,7 @@
 from functools import partial
 from getpass import getuser
 from operator import itemgetter
+from pathlib import Path
 from textwrap import dedent
 from typing import Optional
 from urllib.parse import unquote, urlparse, urlunparse
@@ -29,7 +30,7 @@
 import tornado.options
 from dateutil.parser import parse as parse_date
 from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
-from jupyter_telemetry.eventlog import EventLog
+from jupyter_events.logger import EventLogger
 from sqlalchemy.exc import OperationalError, SQLAlchemyError
 from sqlalchemy.orm import joinedload
 from tornado import gen, web
@@ -3251,13 +3252,10 @@ def init_pycurl(self):
 
     def init_eventlog(self):
         """Set up the event logging system."""
-        self.eventlog = EventLog(parent=self)
+        self.eventlog = EventLogger(parent=self)
 
-        for dirname, _, files in os.walk(os.path.join(here, 'event-schemas')):
-            for file in files:
-                if not file.endswith('.yaml'):
-                    continue
-                self.eventlog.register_schema_file(os.path.join(dirname, file))
+        for schema in (Path(here) / "event-schemas").glob("**/*.yaml"):
+            self.eventlog.register_event_schema(schema)
 
     def write_pid_file(self):
         pid = os.getpid()
diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py
--- a/jupyterhub/handlers/base.py
+++ b/jupyterhub/handlers/base.py
@@ -1128,10 +1128,13 @@ async def finish_user_spawn():
             SERVER_SPAWN_DURATION_SECONDS.labels(
                 status=ServerSpawnStatus.success
             ).observe(time.perf_counter() - spawn_start_time)
-            self.eventlog.record_event(
-                'hub.jupyter.org/server-action',
-                1,
-                {'action': 'start', 'username': user.name, 'servername': server_name},
+            self.eventlog.emit(
+                schema_id='https://schema.jupyter.org/jupyterhub/events/server-action',
+                data={
+                    'action': 'start',
+                    'username': user.name,
+                    'servername': server_name,
+                },
             )
             proxy_add_start_time = time.perf_counter()
             spawner._proxy_pending = True
@@ -1334,10 +1337,9 @@ async def stop():
                 SERVER_STOP_DURATION_SECONDS.labels(
                     status=ServerStopStatus.success
                 ).observe(toc - tic)
-                self.eventlog.record_event(
-                    'hub.jupyter.org/server-action',
-                    1,
-                    {
+                self.eventlog.emit(
+                    schema_id='https://schema.jupyter.org/jupyterhub/events/server-action',
+                    data={
                         'action': 'stop',
                         'username': user.name,
                         'servername': server_name,


Test patch:
diff --git a/jupyterhub/tests/test_eventlog.py b/jupyterhub/tests/test_eventlog.py
--- a/jupyterhub/tests/test_eventlog.py
+++ b/jupyterhub/tests/test_eventlog.py
@@ -19,20 +19,22 @@
 # and `invalid_events` dictionary below.
 
 # To test valid events, add event item with the form:
-# { ( '<schema id>', <version> ) : { <event_data> } }
+# ( '<schema id>', { <event_data> } )
 valid_events = [
     (
-        'hub.jupyter.org/server-action',
-        1,
+        'https://schema.jupyter.org/jupyterhub/events/server-action',
         dict(action='start', username='test-username', servername='test-servername'),
     )
 ]
 
 # To test invalid events, add event item with the form:
-# { ( '<schema id>', <version> ) : { <event_data> } }
+# ( '<schema id>', { <event_data> } )
 invalid_events = [
     # Missing required keys
-    ('hub.jupyter.org/server-action', 1, dict(action='start'))
+    (
+        'https://schema.jupyter.org/jupyterhub/events/server-action',
+        dict(action='start'),
+    )
 ]
 
 
@@ -41,11 +43,11 @@ def eventlog_sink(app):
     """Return eventlog and sink objects"""
     sink = io.StringIO()
     handler = logging.StreamHandler(sink)
-    # Update the EventLog config with handler
+    # Update the EventLogger config with handler
     cfg = Config()
-    cfg.EventLog.handlers = [handler]
+    cfg.EventLogger.handlers = [handler]
 
-    with mock.patch.object(app.config, 'EventLog', cfg.EventLog):
+    with mock.patch.object(app.config, 'EventLogger', cfg.EventLogger):
         # recreate the eventlog object with our config
         app.init_eventlog()
         # return the sink from the fixture
@@ -54,12 +56,12 @@ def eventlog_sink(app):
     app.init_eventlog()
 
 
-@pytest.mark.parametrize('schema, version, event', valid_events)
-def test_valid_events(eventlog_sink, schema, version, event):
+@pytest.mark.parametrize('schema, event', valid_events)
+def test_valid_events(eventlog_sink, schema, event):
     eventlog, sink = eventlog_sink
     eventlog.allowed_schemas = [schema]
     # Record event
-    eventlog.record_event(schema, version, event)
+    eventlog.emit(schema_id=schema, data=event)
     # Inspect consumed event
     output = sink.getvalue()
     assert output
@@ -68,11 +70,11 @@ def test_valid_events(eventlog_sink, schema, version, event):
     assert data is not None
 
 
-@pytest.mark.parametrize('schema, version, event', invalid_events)
-def test_invalid_events(eventlog_sink, schema, version, event):
+@pytest.mark.parametrize('schema, event', invalid_events)
+def test_invalid_events(eventlog_sink, schema, event):
     eventlog, sink = eventlog_sink
     eventlog.allowed_schemas = [schema]
 
     # Make sure an error is thrown when bad events are recorded
     with pytest.raises(jsonschema.ValidationError):
-        recorded_event = eventlog.record_event(schema, version, event)
+        recorded_event = eventlog.emit(schema_id=schema, data=event)