├── src ├── main │ └── java │ │ ├── com │ │ └── sensorsdata │ │ │ └── analytics │ │ │ └── extractor │ │ │ └── processor │ │ │ └── ExtProcessor.java │ │ └── cn │ │ └── sensorsdata │ │ └── sample │ │ └── SampleExtProcessor.java └── test │ └── java │ └── cn │ └── sensorsdata │ └── sample │ └── SampleExtProcessorTest.java ├── .gitignore ├── pom.xml └── README.md /src/main/java/com/sensorsdata/analytics/extractor/processor/ExtProcessor.java: -------------------------------------------------------------------------------- 1 | package com.sensorsdata.analytics.extractor.processor; 2 | 3 | public interface ExtProcessor { 4 | String process(String record) throws Exception; 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | git.properties 3 | *.userlibraries 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 7 | 8 | *.iml 9 | *.eml 10 | 11 | ## Directory-based project format: 12 | .idea/ 13 | 14 | ## File-based project format: 15 | *.ipr 16 | *.iws 17 | 18 | ## Plugin-specific files: 19 | 20 | # IntelliJ 21 | /out/ 22 | 23 | # mpeltonen/sbt-idea plugin 24 | .idea_modules/ 25 | 26 | # JIRA plugin 27 | atlassian-ide-plugin.xml 28 | 29 | # java build files 30 | target 31 | 32 | *.swp 33 | .DS_Store 34 | 35 | # AppCode 36 | .idea 37 | -------------------------------------------------------------------------------- /src/test/java/cn/sensorsdata/sample/SampleExtProcessorTest.java: -------------------------------------------------------------------------------- 1 | package cn.sensorsdata.sample; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.junit.Test; 8 | 9 | /** 10 | * Created by fengjiajie on 17/4/10. 11 | */ 12 | public class SampleExtProcessorTest { 13 | 14 | @Test public void testProcess() throws Exception { 15 | StringBuilder stringBuilder = new StringBuilder(); 16 | stringBuilder.append(" {"); 17 | stringBuilder.append(" \"distinct_id\":\"2b0a6f51a3cd6775\","); 18 | stringBuilder.append(" \"time\":1434556935000,"); 19 | stringBuilder.append(" \"type\":\"track\","); 20 | stringBuilder.append(" \"event\":\"ViewProduct\","); 21 | stringBuilder.append(" \"properties\":{"); 22 | stringBuilder.append(" \"product_name\":\"苹果\""); 23 | stringBuilder.append(" }"); 24 | stringBuilder.append(" }"); 25 | 26 | SampleExtProcessor sampleExtProcessor = new SampleExtProcessor(); 27 | ObjectMapper objectMapper = new ObjectMapper(); 28 | 29 | String processResult = sampleExtProcessor.process(stringBuilder.toString()); 30 | JsonNode recordNode = objectMapper.readTree(processResult); 31 | 32 | assertEquals("添加的字段应该是水果", "水果", recordNode.get("properties").get("product_classify").asText()); 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/cn/sensorsdata/sample/SampleExtProcessor.java: -------------------------------------------------------------------------------- 1 | package cn.sensorsdata.sample; 2 | 3 | import com.sensorsdata.analytics.extractor.processor.ExtProcessor; 4 | 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.node.ObjectNode; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | /** 12 | * Created by fengjiajie on 16/9/28. 13 | */ 14 | public class SampleExtProcessor implements ExtProcessor { 15 | 16 | private static final Logger logger = LoggerFactory.getLogger(SampleExtProcessor.class); 17 | 18 | private ObjectMapper objectMapper = new ObjectMapper(); 19 | 20 | public SampleExtProcessor() { 21 | } 22 | 23 | public String process(String record) throws Exception { 24 | // 传入参数为一条符合 Sensors Analytics 数据格式定义的 Json 25 | // 数据格式定义 https://www.sensorsdata.cn/manual/data_schema.html 26 | JsonNode recordNode = objectMapper.readTree(record); 27 | ObjectNode propertiesNode = (ObjectNode) recordNode.get("properties"); 28 | 29 | // 例如传入的一条需要处理的数据是: 30 | // 31 | // { 32 | // "distinct_id":"2b0a6f51a3cd6775", 33 | // "time":1434556935000, 34 | // "type":"track", 35 | // "event":"ViewProduct", 36 | // "properties":{ 37 | // "product_name":"苹果" 38 | // } 39 | // } 40 | // 41 | // 如果是“苹果”或“梨”, 那么添加一个字段标记产品为“水果”; 42 | // 如果是“萝卜”或“白菜”, 那么标记为“蔬菜”; 43 | 44 | if (propertiesNode.has("product_name")) { 45 | String productName = propertiesNode.get("product_name").asText(); 46 | if ("苹果".equals(productName) || "梨".equals(productName)) { 47 | propertiesNode.put("product_classify", "水果"); 48 | // 输出日志到 /data/sa_standalone/logs/extractor 下的 extractor.log 中 49 | logger.info("Find a fruit: {}", productName); 50 | } else if ("萝卜".equals(productName) || "白菜".equals(productName)) { 51 | propertiesNode.put("product_classify", "蔬菜"); 52 | } 53 | } 54 | 55 | return objectMapper.writeValueAsString(recordNode); 56 | } 57 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.sensorsdata.sample 8 | ext-processor-sample 9 | 0.1 10 | 11 | 12 | 13 | com.fasterxml.jackson.core 14 | jackson-databind 15 | 2.5.3 16 | 17 | 18 | 19 | org.slf4j 20 | slf4j-log4j12 21 | 1.7.12 22 | 23 | 24 | 25 | junit 26 | junit 27 | 4.12 28 | test 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.apache.maven.plugins 36 | maven-shade-plugin 37 | 2.4.3 38 | 39 | 40 | package 41 | 42 | shade 43 | 44 | 45 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-compiler-plugin 61 | 3.3 62 | 63 | 1.8 64 | 1.8 65 | UTF-8 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExtProcessor 数据预处理模块 2 | 3 | ## 1. 概述 4 | 5 | Sensors Analytics 从 1.6 开始为用户开放自定义“数据预处理模块”,即为 SDK 等方式接入的数据(不包括批量导入工具方式)提供一个简单的 ETL 流程,使数据接入更加灵活。 6 | 7 | 可以使用“数据预处理模块”处理的数据来源包括: 8 | 9 | * SDK(各语言 SDK 直接发送的数据); 10 | * LogAgent; 11 | * FormatImporter; 12 | 13 | 使用 BatchImporter 和 HdfsImporter 批量导入数据的情况除外。 14 | 15 | 例如 SDK 发来一条数据,传入“数据预处理模块”时格式如下: 16 | 17 | ```json 18 | { 19 | "distinct_id":"2b0a6f51a3cd6775", 20 | "time":1434556935000, 21 | "type":"track", 22 | "event":"ViewProduct", 23 | "project": "default", 24 | "ip":"123.123.123.123", 25 | "properties":{ 26 | "product_name":"苹果" 27 | } 28 | } 29 | ``` 30 | 31 | 这时希望增加一个字段 `product_classify`,表示产品的分类,可通过“数据预处理模块”将数据处理成: 32 | 33 | ```json 34 | { 35 | "distinct_id":"2b0a6f51a3cd6775", 36 | "time":1434556935000, 37 | "type":"track", 38 | "event":"ViewProduct", 39 | "project": "default", 40 | "properties":{ 41 | "product_name":"苹果", 42 | "product_classify":"水果" 43 | } 44 | } 45 | ``` 46 | 47 | ## 2. 开发方法 48 | 49 | 一个“数据预处理模块”需要自定义一个 Java 类实现 `com.sensorsdata.analytics.extractor.processor.ExtProcessor` 接口,该接口定义如下: 50 | 51 | [ExtProcessor.java](https://github.com/sensorsdata/ext-processor-sample/blob/master/src/main/java/com/sensorsdata/analytics/extractor/processor/ExtProcessor.java) : 52 | 53 | ```java 54 | package com.sensorsdata.analytics.extractor.processor; 55 | 56 | public interface ExtProcessor { 57 | String process(String record) throws Exception; 58 | } 59 | ``` 60 | 61 | * 参数: 一条符合 [Sensors Analytics 的数据格式定义](https://www.sensorsdata.cn/manual/data_schema.html)的 JSON 文本,例如概述中的第一个 JSON。与 [数据格式](https://www.sensorsdata.cn/manual/data_schema.html) 唯一区别在于数据中包含字段 `ip` ,值为接收数据时取到的客户端 IP; 62 | * 返回值: 经过处理后的 JSON 或 JSON 数组,例如概述中的第二个 JSON。其格式需要符合 [Sensors Analytics 的数据格式定义](https://www.sensorsdata.cn/manual/data_schema.html); 如果返回值包含多条数据,可返回一个 JSON 数组,数组中的每个元素为一条符合 [数据格式](https://www.sensorsdata.cn/manual/data_schema.html) 的数据; 若返回值为 `null`,表示抛弃这条数据; 63 | * 异常: 抛出异常将导致这条数据被抛弃并输出错误日志; 64 | 65 | 本 repo 提供了一个完整的“数据预处理模块”样例代码,用于实现“概述”中所描述的样例场景,定义接口文件: 66 | 67 | [ExtProcessor.java](https://github.com/sensorsdata/ext-processor-sample/blob/master/src/main/java/com/sensorsdata/analytics/extractor/processor/ExtProcessor.java) 68 | 69 | 实现自定义处理逻辑的类文件: 70 | 71 | [SampleExtProcessor.java](https://github.com/sensorsdata/ext-processor-sample/blob/master/src/main/java/cn/sensorsdata/sample/SampleExtProcessor.java) 72 | 73 | * 在开发其他项目时,在合适的目录下添加 [ExtProcessor.java](https://github.com/sensorsdata/ext-processor-sample/blob/master/src/main/java/com/sensorsdata/analytics/extractor/processor/ExtProcessor.java) 文件并实现接口即可。 74 | 75 | ## 2.1 开发常见问题 76 | 77 | * 如果使用了 log4j (或 slf4j) 日志库,日志将输出到 `/data/sa_cluster/logs/extractor` (其中 `/data` 为数据盘挂载点,请根据实际情况替换) 下的 `extractor.log` 中,并且不支持自定义 log4j 配置。若必须将日志输出到指定位置,可直接在代码中 `FileOutputStream` 写文件或使用其他日志库; 78 | * 如果想要抛弃一条数据,`process` 函数直接返回 `null` 即可; 79 | * 如希望一次处理返回多条数据(例如一条传入数据输出多条数据,或传入多条数据批处理再全部输出),请返回一个 JSON 数组,数组中的每个元素都为符合 [Sensors Analytics 的数据格式定义](https://www.sensorsdata.cn/manual/data_schema.html) 的数据: 80 | ```json 81 | [ 82 | { 83 | "distinct_id":"2b0a6f51a3cd6775", 84 | "time":1434556935000, 85 | "type":"track", 86 | "event":"ViewProduct", 87 | "project": "sample_project", 88 | "properties":{ 89 | ... 90 | } 91 | }, 92 | { 93 | "distinct_id":"2b0a6f51a3cd6775", 94 | "type":"profile_set", 95 | "time":1434556935000, 96 | "project": "sample_project", 97 | "properties":{ 98 | "is_vip":true 99 | } 100 | } 101 | ] 102 | ``` 103 | * 请注意**空指针的问题**,比如某个需要处理的 `property` 不是每条数据都存在,如果不存在时取值并使用可能造成空指针异常,如果不在处理模块内部处理该异常直接抛出,将导致这条数据被抛弃; 104 | * 请注意用户属性数据即 `type` 以 `profile_` 开头的数据,是没有 `event` 字段的,若用到 `event` 字段,请先判断字段是否存在; 105 | * 请用尽量多的判断以确定一条数据是否是你希望修改的数据再做操作; 106 | * 一条数据若不需要修改直接返回原文本即可; 107 | * 一般情况下,Sensors Analytics 每台机器实时导入速度最高可以达到约每秒 5k ~ 30k 条(受数据字段数、机器性能等影响而不同),若使用“数据预处理模块”可能带来额外的性能开销,建议使用前对“数据预处理模块”性能进行评估,更多对性能的影响请参考 [性能](https://github.com/sensorsdata/ext-processor-sample#7-%e6%80%a7%e8%83%bd); 108 | * 极端情况下(如模块重启)同一条数据可能被“数据预处理模块”多次处理。若使用“数据预处理模块”的目的如本 repo 仅添加字段,那么多次处理没有影响,但若是在“数据预处理模块”中做统计等操作(不建议这样做,统计需求建议通过订阅 kafka 数据实现),则需考虑重复执行的影响; 109 | * 导入模块通过反射实例化用户指定的预处理类,并通过接口进行访问。不建议在“数据预处理模块”中包含复杂逻辑,此部分的问题需相关开发人员自行调试; 110 | * 预处理类在一个 JVM 内可能被实例化多次,对于一个实例不会被多线程同时访问,但多个实例有可能被同时访问; 111 | 112 | ## 3. 编译打包 113 | 114 | 用于部署的“数据预处理模块”需要打成一个 JAR 包。 115 | 116 | 本 repo 附带的样例使用了 Jackson 库解析 JSON,并使用 Maven 做包管理,编译并打包本 repo 代码可通过: 117 | 118 | ```bash 119 | git clone git@github.com:sensorsdata/ext-processor-sample.git 120 | cd ext-processor-sample 121 | mvn clean package 122 | ``` 123 | 124 | 执行编译后可在 `target` 目录下找到 `ext-processor-sample-0.1.jar`。 125 | 126 | ## 4. 测试 JAR 127 | 128 | ext-processor-utils 是用于测试、部署“数据预处理模块”的工具,只能运行于部署 Sensors Analytics 的机器上。 129 | 130 | 将编译出的 JAR 文件上传到部署 Sensors Analytics 的机器上,例如 `ext-processor-sample-0.1.jar`。 131 | 132 | 切换到 `sa_cluster` 账户: 133 | 134 | ```bash 135 | sudo su - sa_cluster 136 | ``` 137 | 138 | 直接运行 ext-processor-utils 将输出参数列表如: 139 | 140 | ``` 141 | ~/sa/extractor/bin/ext-processor-utils 142 | 143 | usage: [ext-processor-utils] [-c ] [-h] [-j ] -m 144 | -c,--class 实现 ExtProcessor 的类名, 例如 145 | cn.kbyte.CustomProcessor 146 | -h,--help help 147 | -j,--jar 包含 ExtProcessor 的 jar, 例如 148 | custom-processor-0.1.jar 149 | -m,--method 操作类型, 可选 150 | test/run/install/uninstall/info/ 151 | run_with_real_time_data 152 | test: 测试 jar 是否可加载; 153 | run: 运行指定 class 类的 process 方法, 154 | 以标准输入的逐行数据作为参数输入, 将返回结果输出到标准输出; 155 | run_with_real_time_data: 156 | 使用本机实时的数据作为输入, 将返回结果输出到标准输出; 157 | install: 安装 ExtProcessor; 158 | uninstall: 卸载 ExtProcessor; 159 | info: 查看当前配置状态; 160 | -t,--add_in_track_signup 是否将预处理应用于 track signup 的单独处理流中. 161 | yes 表示,再打开 track signup 162 | 的处理流的前提下,会同时将预处理的内容也添加到 track 163 | signup 流中, no 表示在 track signup 164 | 流中不进行预处理。 165 | 如果您的预处理会影响到 track_signup 166 | 的结果(例如,会修改 distinct_id 等),请打开此开关 167 | --when_exception_use_original 当 ExtProcessor 168 | 抛异常时导入原始数据而不是直接抛弃, yes 169 | 表示预处理遇到异常时使用原始数据导入, no 170 | 表示遇到异常时抛弃该条数据 171 | ``` 172 | 173 | 使用 `test` 方法测试 JAR 并加载 Class: 174 | 175 | ```bash 176 | ~/sa/extractor/bin/ext-processor-utils \ 177 | --jar ext-processor-sample-0.1.jar \ 178 | --class cn.sensorsdata.sample.SampleExtProcessor \ 179 | --method test 180 | ``` 181 | 182 | * `jar`: JAR 包路径; 183 | * `class`: 实现 `com.sensorsdata.analytics.extractor.processor.ExtProcessor` 的 Java 类; 184 | 185 | 输出如下: 186 | 187 | ``` 188 | 16/10/15 18:27:51 main INFO utils.ExtLibUtils: 加载 jar: /home/sa_cluster/ext-processor-sample-0.1.jar, class: cn.sensorsdata.sample.SampleExtProcessor 成功 189 | ``` 190 | 191 | ### 4.1 测试运行 192 | 193 | 使用 `run` 方法加载 JAR 并实例化 Class,以标准输入的逐行数据作为预处理函数输入,并将处理结果输出到标准输出: 194 | 195 | ```bash 196 | ~/sa/extractor/bin/ext-processor-utils \ 197 | --jar ext-processor-sample-0.1.jar \ 198 | --class cn.sensorsdata.sample.SampleExtProcessor \ 199 | --method run 200 | ``` 201 | 202 | ### 4.2 以线上实时数据测试运行 203 | 204 | 使用 `run_with_real_time_data` 方法加载 JAR 并实例化 Class,以本机实际接收的数据作为预处理函数输入,并将输入和输出打印到标准输出: 205 | 206 | ```bash 207 | ~/sa/extractor/bin/ext-processor-utils \ 208 | --jar ext-processor-sample-0.1.jar \ 209 | --class cn.sensorsdata.sample.SampleExtProcessor \ 210 | --method run_with_real_time_data \ 211 | --add_in_track_signup yes \ 212 | --when_exception_use_original no 213 | ``` 214 | 215 | ## 5. 安装 216 | 217 | 使用 ext-processor-utils 的 `install` 方法安装,例如安装样例执行如下命令: 218 | 219 | ```bash 220 | ~/sa/extractor/bin/ext-processor-utils \ 221 | --jar ext-processor-sample-0.1.jar \ 222 | --class cn.sensorsdata.sample.SampleExtProcessor \ 223 | --method install \ 224 | --add_in_track_signup yes \ 225 | --when_exception_use_original yes 226 | ``` 227 | 228 | * 由于涉及内部模块启停,安装时请耐心等待; 229 | * 集群版安装预处理模块会自动分发,不需要每台机器操作; 230 | * 若已经安装过“数据预处理模块”,再次执行“安装”操作将替换使用新的 JAR 包; 231 | * add_in_track_signup 设置为 yes,可以将预处理对于数据操作同时加入到单独的 TrackSignup 流中,当预处理会影响到 TrackSignup 时(比如会修改 distinct_id 等情况时),需要设置为 yes。否则请设置为 no。 232 | * when_exception_use_original 设置为 yes 可以避免未考虑到的空指针异常使数据无法导入,但副作用是这条数据没有经过预处理,可能不符合预期而无法用于查询甚至产生脏数据; 233 | 234 | ## 6. 验证 235 | 236 | 安装好“数据预处理模块”后,为了验证处理结果是否符合预期,可以开启 SDK 的 [`Debug 模式`](https://www.sensorsdata.cn/manual/debug_mode.html) 校验数据。 237 | 238 | 1. 使用管理员帐号登录 Sensors Analytics 界面,点击左下角 `埋点`,在新页面中点击右上角 `数据接入辅助工具`,在新页面中点击最上面导航栏中的 `DEBUG数据查看`; 239 | 2. 配置 SDK 使用 [`Debug 模式`](https://www.sensorsdata.cn/manual/debug_mode.html); 240 | 3. 发送一条测试用的数据,观察是否进行了预期处理即可; 241 | 242 | ## 7. 性能 243 | 244 | 执行命令输出导入模块的统计信息: 245 | 246 | ```bash 247 | sa_admin status -m extractor 248 | ``` 249 | 250 | 其中 extProcessorBottleneck 为当前数据预处理模块的性能瓶颈,其计算方法如下: 251 | 252 | ```java 253 | /** 初始化统计 **/ 254 | processUseTime = 0; // 用于统计执行预处理所用时间 255 | processCount = 0; // 用于统计调用预处理次数 256 | 257 | /** 每次预处理 **/ 258 | start = System.nanoTime(); // 记录执行预处理前的时间戳 259 | record = serializeToJson(rawRecord); // 将数据序列化成 JSON 格式 260 | extProcessor.process(record); // 调用预处理函数 261 | processUseTime += System.nanoTime() - start; // 累加本次执行预处理消耗的时间到总和 262 | ++processCount; // 累加执行次数 263 | 264 | /** 每 1 分钟计算上 1 分钟统计值,并清零计数 **/ 265 | // 得出每秒最多执行 process() 次数,即此处的性能瓶颈 266 | extProcessorBottleneck = processCount * 1000000000L / processUseTime; 267 | processUseTime = 0; 268 | processCount = 0; 269 | ``` 270 | 271 | 1. 此处计算出来的值 extProcessorBottleneck 相当于仅执行预处理每秒最多多少次,由于导入还有很多其他数据处理步骤,故总导入速度将小于该值; 272 | 2. 导入模块分配的内存有限,若预处理需要消耗较多内存,请提前联系我们调大内存参数,若过多将影响其他模块运行; 273 | 274 | ## 8. 卸载 275 | 276 | 若不再需要“数据预处理模块”,可以通过 ext-processor-utils 的 `uninstall` 方法卸载,执行如下命令: 277 | 278 | ```bash 279 | ~/sa/extractor/bin/ext-processor-utils --method uninstall 280 | ``` 281 | 282 | * 若希望更新 JAR 包,请直接使用工具“安装”新的 JAR 包即可,不需要先进行卸载; 283 | --------------------------------------------------------------------------------