缘起

最近在折腾Open edX数据相关的东西,同时试着将open edx与机器学习结合

如果你是EDM(educational data mining)的新玩家(我也是新手233),对教育领域的数据感兴趣,推荐你这篇论文《教育数据挖掘研究进展综述》,了解一下这个领域的概况和进展

官方的insights显得太重,我们打算享用这个免费午餐,但不定制它。对于个性化的数据分析,我们准备另起炉灶,完全独立于Open edX本身。架构上的设计主要为

  1. 采用edx的tracking system(这套机制的设计十分聪明),但产生独立的log,这样一来既不污染insights的数据源,分析时也避免从一大推的数据里去过滤数据
  2. 为了对接到edx本身,诸如需要获取课程和用户数据,采用我之前写的edx_siteapi与Open edX系统通信。
  3. 将其作为一个独立的服务

我用周末的两天,搭建了初始模型,从目前的进展看来,这条路似乎还算康庄大道

任务描述

架构中的第一条,产生了难题,我试着去hack tracking system的时候,发现它并不是个平坦的结构,hack起来十分痛苦,而我定制开源系统的原则之一是尽可能不侵入原代码。尽可能尊重原有架构,试着去理解它而不是急着改造它,后者的代价太沉重了,危机四伏

所以眼前的第一个任务是理解tracking system的机制,并优雅地定制它

最初思路

另起炉灶

写一个rest服务,在js中用ajax去发送事件(当然你也可以用edx采用的backbone),对应的rest服务去写日志(采用django或是python原生的方式都行),这种方式简单粗暴。因为违背了我拓展开源系统的原则,所以直接pass了

拓展既有的tracking system

  • best-practices

    1
    2
    
    from eventtracking import tracker
    tracker.emit('some.event.name', {'foo': 'bar'}) #未注册的事件

问题是日志没有单独剥离出来,分析时,日志量巨大,需要筛选出来,所以这是我们主要需要解决的问题

更好的范式为

1
2
3
from eventtracking import tracker
with tracker.get_tracker().context(event_name, context):
    tracker.emit(event_name, event_data)

Open edX tracking system的设计

Other Tracking Systems(edx支持)

  • Data Dog: 一种系统监控工具,应用监控服务商(saas)
    • airbnb在用
  • Segment: 希望做出一款能与KISSmetrics或Google Analytics展开有力竞争的产品
  • Google Analytics
  • Deprecated APIs

出于国内网络的原因,我们可能需要考虑国内的Analytics服务,由于Analytics用种普遍需求,所以国内也容易找到同类产品:

Open edX中的tracking log

Open edX中的tracking system产生的log与系统业务相关,数据特征皆为教育者所关心。并不是一种不是通用的数据(诸如nginx产生的日志就很通用,仅关心http请求本身),正是这种特质,使其适合作为EDM的数据源

源码分析

由于当前文档的匮乏,我们只好直接翻源码了,涉及的源码主要包括

我们从产生emit(从语义就可以知道这是提交日志事件的功能函数)开始追踪,目的是找到产生独立log的方法:

从class的名字我们可以猜到这个类(Tracker)基本就是我们要找的东西啦

接下来找侵入的地方:

  • -> tracker.get_tracker()

    1
    2
    
    from eventtracking import tracker  #实际上是eventtracking/tracker.py文件
    tracker = tracker.get_tracker() #得到TRACKERS['default'] 在register_tracker赋值 ,产生疑惑:register_tracker何时被注册的
  • -> eventtracking/django/__init__.py 里override_default_tracker()注册tracker,而eventtracking.djangoEventTrackingConfig是触发上边代码的地方,具体原理可见:

    1
    2
    3
    4
    
    #cms/envs/common.py
    # Tracking
    'track',
    'eventtracking.django.apps.EventTrackingConfig',  #这里最终触发注册

所以我们完全可以构造自己的tracker,因为TRACKERS是个dict,所以可以同时存在任意多的tracker,每个tracker可以有自己的backends,至此,探索完毕,问题解决


至于细节问题,构造tracker是主要的任务,直接用Tracker构造一个新的就好,核心参数是backends, ThreadLocalContextLocator(), processors,其中backends是定制化的核心,之后加入到TRACKERS中,调用时,from eventtracking import mytracker就好,其他和原生的tracker无异

backend and middleware

既然在读这块的源码,解决完任务,顺便做了更全面的探索(从框架层)

backend

1
2
3
4
#lms/envs/aws.py
# Event tracking
TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {}))
EVENT_TRACKING_BACKENDS['tracking_logs']['OPTIONS']['backends'].update(AUTH_TOKENS.get("EVENT_TRACKING_BACKENDS", {}))

middleware

edx-platform/common/djangoapps/track/middleware.py中,tracker在request里产生,被注入了session之类的属性,tracker生成的时候就很强大了,这就是它有上下文的原因,django作为一个大而全的框架,在此威力在此显现出来

其他

server_track

这个应该是配合js使用的,作为rest服务,这部分没详细去挖掘,我想模仿Segment的机制去实现

xblock tracking system

直接参考既有的插件就行了,这一部分倒是简单

参考