Skip to content

Commit

Permalink
feat: jupyterlab telemetry system
Browse files Browse the repository at this point in the history
  • Loading branch information
mengyanw committed Sep 12, 2023
1 parent 68495f1 commit fc9954a
Show file tree
Hide file tree
Showing 9 changed files with 1,423 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# This file should be saved into one of the config directories provided by `jupyter --path`.

c.JupyterLabTelemetrySystemApp.activeEvents = [
'NotebookOpenEvent',
'NotebookScrollEvent',
'NotebookVisibleEvent',
'NotebookHiddenEvent',
'ClipboardCopyEvent',
'ClipboardCutEvent',
'ClipboardPasteEvent',
'ActiveCellChangeEvent',
'NotebookSaveEvent',
'CellExecuteEvent',
'CellAddEvent',
'CellRemoveEvent',
]

c.JupyterLabTelemetrySystemApp.logNotebookContentEvents = [
'NotebookOpenEvent',
# 'NotebookScrollEvent',
# 'NotebookVisibleEvent',
# 'NotebookHiddenEvent',
# 'ClipboardCopyEvent',
# 'ClipboardCutEvent',
# 'ClipboardPasteEvent',
# 'ActiveCellChangeEvent',
'NotebookSaveEvent',
# 'CellExecuteEvent',
# 'CellAddEvent',
# 'CellRemoveEvent',
]

# This file should be saved into one of the config directories provided by `jupyter --path`.
from jupyterlab_telemetry_system import handlers

def customized_exporter(args):
# do more here
return ({
'exporter': 'CustomizedCommandLineExporter',
})

c.JupyterLabTelemetrySystemApp.exporters = [
{
'exporter': handlers.console_exporter,
},
{
'exporter': handlers.command_line_exporter,
},
{
'exporter': handlers.file_exporter,
'args': {
'path': 'log'
}
},
{
'exporter': handlers.remote_exporter,
'args': {
'id': 'S3Exporter',
'url': 'https://telemetry.mentoracademy.org/telemetry-edtech-labs-si-umich-edu/dev/test-telemetry',
'env': ['WORKSPACE_ID'],
}
},
{
'exporter': handlers.remote_exporter,
'args': {
'id': 'MongoDBLambdaExporter',
'url': 'https://68ltdi5iij.execute-api.us-east-1.amazonaws.com/mongo',
'params': {
'mongo_cluster': 'mengyanclustertest.6b83fsy.mongodb.net',
'mongo_db': 'telemetry',
'mongo_collection': 'dev'
},
'env': ['WORKSPACE_ID'],
}
},
{
'exporter': handlers.remote_exporter,
'args': {
'id': 'InfluxDBLambdaExporter',
'url': 'https://68ltdi5iij.execute-api.us-east-1.amazonaws.com/influx',
'params': {
'influx_bucket': 'telemetry_dev',
'influx_measurement': 'si101_fa24'
}
}
},
{
'exporter': customized_exporter,
'args': {
# do more here
}
},
]
19 changes: 4 additions & 15 deletions jupyterlab_telemetry_system/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,19 @@
import warnings
warnings.warn("Importing 'jupyterlab_telemetry_system' outside a proper installation.")
__version__ = "dev"
from .handlers import setup_handlers

from .application import JupyterLabTelemetrySystemApp

def _jupyter_labextension_paths():
return [{
"src": "labextension",
"dest": "jupyterlab-telemetry-system"
}]


def _jupyter_server_extension_points():
return [{
"module": "jupyterlab_telemetry_system"
"module": "jupyterlab_telemetry_system",
"app": JupyterLabTelemetrySystemApp
}]


def _load_jupyter_server_extension(server_app):
"""Registers the API handler to receive HTTP requests from the frontend extension.
Parameters
----------
server_app: jupyterlab.labapp.LabApp
JupyterLab application instance
"""
setup_handlers(server_app.web_app)
name = "jupyterlab_telemetry_system"
server_app.log.info(f"Registered {name} server extension")
load_jupyter_server_extension = JupyterLabTelemetrySystemApp.load_classic_server_extension
27 changes: 27 additions & 0 deletions jupyterlab_telemetry_system/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from .handlers import RouteHandler
from jupyter_server.extension.application import ExtensionApp
from traitlets import List

class JupyterLabTelemetrySystemApp(ExtensionApp):

name = "jupyterlab_telemetry_system"

activeEvents = List([]).tag(config=True)
logNotebookContentEvents = List([]).tag(config=True)
exporters = List([]).tag(config=True)

def initialize_settings(self):
try:
assert self.activeEvents, "The c.JupyterLabTelemetrySystemApp.activeEvents configuration setting must be set."
assert self.exporters, "The c.JupyterLabTelemetrySystemApp.exporters configuration must be set, please see the configuration example"

except Exception as e:
self.log.error(str(e))
raise e

def initialize_handlers(self):
try:
self.handlers.extend([(r"/jupyterlab-telemetry-system/(.*)", RouteHandler)])
except Exception as e:
self.log.error(str(e))
raise e
120 changes: 105 additions & 15 deletions jupyterlab_telemetry_system/handlers.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,114 @@
import json
from ._version import __version__
from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.extension.handler import ExtensionHandlerMixin
import os, json, tornado, inspect
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from tornado.httputil import HTTPHeaders
from tornado.escape import to_unicode

from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join
import tornado
# Exporters
def console_exporter(args):
return ({
'exporter': args.get('id') or 'ConsoleExporter',
'message': args['data']
})

def command_line_exporter(args):
print(args['data'])
return ({
'exporter': args.get('id') or 'CommandLineExporter',
})

def file_exporter(args):
f = open(args.get('path'), 'a+', encoding='utf-8')
json.dump(args['data'], f, ensure_ascii=False, indent=4)
f.write(',')
f.close()
return({
'exporter': args.get('id') or 'FileExporter',
})

async def remote_exporter(args):
http_client = AsyncHTTPClient()
request = HTTPRequest(
url=args.get('url'),
method='POST',
body=json.dumps({
'data': args['data'],
'params': args.get('params'), # none if exporter does not contain 'params'
'env': [{x: os.getenv(x)} for x in args.get('env')] if (args.get('env')) else []
}),
headers=HTTPHeaders({'content-type': 'application/json'})
)
response = await http_client.fetch(request, raise_error=False)
return({
'exporter': args.get('id') or 'RemoteExporter',
'message': {
'code': response.code,
'reason': response.reason,
'body': to_unicode(response.body),
},
})

class RouteHandler(ExtensionHandlerMixin, JupyterHandler):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

class RouteHandler(APIHandler):
# The following decorator should be present on all verb methods (head, get, post,
# patch, put, delete, options) to ensure only authorized user can request the
# Jupyter server
@tornado.web.authenticated
def get(self):
self.finish(json.dumps({
"data": "This is /jupyterlab-telemetry-system/get-example endpoint!"
}))
def get(self, resource):
try:
self.set_header('Content-Type', 'application/json')
if resource == 'version':
self.finish(json.dumps(__version__))
elif resource == 'environ':
self.finish(json.dumps({k:v for k, v in os.environ.items()}))
elif resource == 'config':
self.finish(json.dumps({
"activeEvents": self.extensionapp.activeEvents,
"logNotebookContentEvents": self.extensionapp.logNotebookContentEvents
}))
else:
self.set_status(404)
except Exception as e:
self.log.error(str(e))
self.set_status(500)
self.finish(json.dumps(str(e)))

@tornado.web.authenticated
async def post(self, resource):
try:
if resource == 'export':
result = await self.export()
self.finish(json.dumps(result))
else:
self.set_status(404)

except Exception as e:
self.log.error(str(e))
self.set_status(500)
self.finish(json.dumps(str(e)))

async def export(self):
exporters = self.extensionapp.exporters
data = json.loads(self.request.body)
results = []

for each in exporters:
exporter = each.get('exporter')
args = each.get('args') or {} # id, url, path, params, env
args['data'] = data

if callable(exporter):
result = await exporter(args) if inspect.iscoroutinefunction(exporter) else exporter(args)
results.append(result)
else:
results.append({
'message': '[Error] exporter is not callable'
})

def setup_handlers(web_app):
host_pattern = ".*$"
return results

base_url = web_app.settings["base_url"]
route_pattern = url_path_join(base_url, "jupyterlab-telemetry-system", "get-example")
handlers = [(route_pattern, RouteHandler)]
web_app.add_handlers(host_pattern, handlers)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"dependencies": {
"@jupyterlab/application": "^4.0.0",
"@jupyterlab/coreutils": "^6.0.0",
"@jupyterlab/notebook": "^4.0.5",
"@jupyterlab/services": "^7.0.0"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit fc9954a

Please sign in to comment.