教育大数据-用户浏览行为日志采集¶
实时数据采集流程
flowchart LR
subgraph 数据源
A[📦 mock-data.jar]
end
A -->|写入| B[📄 app.log<br>用户行为日志]
A -->|写入| C[🐬 MySQL<br>业务数据]
subgraph 采集层
D[🔄 Flume]
E[⚡ Maxwell]
end
B -->|TailDir 读取| D
C -->|Binlog 监听| E
subgraph 消息队列
F[📨 Kafka<br>Topic: topic_log]
G[📨 Kafka<br>Topic: topic_db]
end
D -->|发送| F
E -->|发送| G
classDef logBranch fill:#FFA500,stroke:#CC8400,stroke-width:2px,color:#fff;
class B,D,F logBranch;
前置条件¶
- 可使用
mock-data.jar模拟数据 ZooKeeper正常运行Kafka正常运行
项目开发¶
如果日志文件app.log中存在不完整的JSON数据,我们可以使用Flume拦截器将其过滤。我们使用集成开发环境进行开发,比如:
- Intellij IDEA
- Eclipse
创建Maven工程¶
创建Maven工程edu-flume-interceptor
引入依赖和打包插件¶
在pom.xml文件中添加如下配置
<dependencies>
<dependency>
<groupId>org.apache.flume</groupId>
<artifactId>flume-ng-core</artifactId>
<version>1.9.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
创建包¶
创建com.studybigdata.flume.interceptors包。
创建JSONUtils工具类¶
在com.studybigdata.flume.interceptors.utils包下创建JSONUtils类
此工具类用来判断字符串是否能转成JSON对象
import com.alibaba.fastjson.JSONObject;
public class JSONUtil {
public static boolean isJSONValidate(String log) {
try {
JSONObject.parseObject(log);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
创建ETL拦截器类¶
在com.studybigdata.flume.interceptors包下创建ETLInterceptor类,过滤掉Flume读取到的不完整的JSON数据。
public class ETLInterceptor implements Interceptor {
@Override
public void initialize() {
}
/*
* 过滤掉脏数据(不完整的json)
* */
@Override
public Event intercept(Event event) {
// 1 获取 body当中的数据
byte[] body = event.getBody();
String log = new String(body, StandardCharsets.UTF_8);
// 2 判断数据是否是完整的json
if (JSONUtil.isJSONValidate(log)) {
return event;
}
return null;
}
@Override
public List<Event> intercept(List<Event> list) {
Iterator<Event> iterator = list.iterator();
while (iterator.hasNext()) {
Event event = iterator.next();
if (intercept(event) == null) {
iterator.remove();
}
}
return list;
}
@Override
public void close() {
}
public static class Builder implements Interceptor.Builder {
@Override
public Interceptor build() {
return new ETLInterceptor();
}
@Override
public void configure(Context context) {
}
}
}
项目结构如图¶
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─studybigdata
│ │ │ └─flume
│ │ │ └─interceptor
│ │ │ │ ETLInterceptor.java
│ │ │ └─utils
│ │ │ └─JSONUtil.java
│ │ │
│ │ └─resources
├─pom.xml
项目测试¶
构造测试字符串,测试拦截器。
测试用例 1:完整的 JSON 数据应该被保留¶
void testIntercept_ValidJSON() {
// 准备一个完整的 JSON 数据
String validJSON = "{\"event_id\":\"1001\",\"user_id\":\"user123\",\"action\":\"click\",\"timestamp\":1711353600000}";
// 创建 Event
Event event = EventBuilder.withBody(validJSON.getBytes(StandardCharsets.UTF_8));
// 执行拦截
Event result = etlInterceptor.intercept(event);
// 验证:完整的 JSON 应该被保留
System.out.println(result);
System.out.println(new String(result.getBody(), StandardCharsets.UTF_8));
}
测试用例 2:不完整的 JSON 数据应该被过滤掉¶
void testIntercept_InvalidJSON() {
// 准备一个不完整的 JSON 数据(缺少右括号)
String invalidJSON = "{\"event_id\":\"1002\",\"user_id\":\"user456\",\"action\":\"view\"";
// 创建 Event
Event event = EventBuilder.withBody(invalidJSON.getBytes(StandardCharsets.UTF_8));
// 执行拦截
Event result = etlInterceptor.intercept(event);
//打印结果
System.out.println(result)
}
项目打包部署¶
上传到服务器¶
需要先将打好的包放入到node1的/opt/bigdata/flume/default/lib文件夹。
配置Flume Job¶
-
将在模拟数据任务中产生的日志文件作为
source发送到Kafka Channel, -
Kafka Topic设置为topic_log
#根据日志生成文件进行修改
a1.sources.r1.filegroups.f1 = /opt/bigdata/mock/edu/log/app.*
a1.sources.r1.positionFile = /opt/bigdata/mock/edu/log_position.json
a1.sources.r1.interceptors = i1
a1.sources.r1.interceptors.i1.type = com.studybigdata.flume.interceptor.ETLInterceptor$Builder
#使用kafka channel
...
source¶
由于用户的浏览行为产生的日志是不断更新的,请为其配置合适的Source Type。
positionFile¶
[root@node1 edu]# cat log_position.json
[{"inode":33748060,"pos":62921997,"file":"/opt/bigdata/mock/edu/log/app.2026-03-22.log"},{"inode":47476704,"pos":5229864,"file":"/opt/bigdata/mock/edu/log/app.log"}]
positionFile指定一个日志读取进度记录文件,核心是记录日志采集 / 消费程序的读取位置,包含两个日志文件的追踪信息,每个字段含义:
inode:Linux 文件唯一索引节点号(文件改名 / 轮转后,inode 不变,用于精准定位文件);pos:字节偏移量(程序已读取到该文件的第pos个字节,下次从这个位置继续读);file:日志文件的绝对路径(一个是历史归档日志app.2026-03-22.log,一个是当前正在写入的实时日志app.log)。
启动Flume Agent¶
使用Kafka客户端消费Topic¶
flowchart LR
subgraph 数据源
A[📦data<br> mock-data.jar]
end
A -->|写入| B[📄用户行为日志<br> app.log]
subgraph 采集层
D[🔄Flume]
end
B -->|读取| D
subgraph 消息队列
F[📨 Kafka<br>Topic: topic_log]
end
D -->|发送| F
subgraph 消费端
G[💻Consumer Client<br> kafka-console-consumer.sh]
end
F -->|订阅消费| G
classDef logBranch fill:#FFA500,stroke:#CC8400,stroke-width:2px,color:#fff;
class B,D,F logBranch;
class G logBranch;
测试数据¶
app.log
{"id":1,"name":"张三","email":"zhangsan@example.com","isValid":true,"reason":null}
{"id":2,"name":"李四","email":"lisi@example.com","isValid":true,"reason":null}
{"id":3,"name":"王五","email":"wangwu@example.com","isValid":true,"reason":null}
{"id":4,"name":"","email":"invalid-email","isValid":false,"reason":
:5,"name":"赵六","email":null,"isValid":false,"reason":"邮箱不能为空"}
flume console日志¶
flume.root.logger=INFO,console
nent type: SOURCE, name: r1 started
com.alibaba.fastjson.JSONException: unterminated json string, pos 67, line 1, column 68{"id":4,"name":"","email":"invalid-email","isValid":false,"reason":
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1466)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1366)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:599)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1400)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1366)
at com.alibaba.fastjson.JSON.parse(JSON.java:170)
at com.alibaba.fastjson.JSON.parse(JSON.java:180)
at com.alibaba.fastjson.JSON.parse(JSON.java:149)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:241)
at com.studybigdata.flume.interceptor.utils.JSONUtil.isJSONValidate(JSONUtil.java:8)
at com.studybigdata.flume.interceptor.ETLInterceptor.intercept(ETLInterceptor.java:28)
at com.studybigdata.flume.interceptor.ETLInterceptor.intercept(ETLInterceptor.java:39)
at org.apache.flume.interceptor.InterceptorChain.intercept(InterceptorChain.java:62)
at org.apache.flume.channel.ChannelProcessor.processEventBatch(ChannelProcessor.java:148)
at org.apache.flume.source.taildir.TaildirSource.tailFileProcess(TaildirSource.java:276)
at org.apache.flume.source.taildir.TaildirSource.process(TaildirSource.java:239)
at org.apache.flume.source.PollableSourceRunner$PollingRunner.run(PollableSourceRunner.java:133)
at java.lang.Thread.run(Thread.java:750)
com.alibaba.fastjson.JSONException: syntax error, pos 1, line 1, column 2:5,"name":"赵六","email":null,"isValid":false,"reason":"邮箱不能为空"}
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1480)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1366)
at com.alibaba.fastjson.JSON.parse(JSON.java:170)
at com.alibaba.fastjson.JSON.parse(JSON.java:180)
at com.alibaba.fastjson.JSON.parse(JSON.java:149)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:241)
at com.studybigdata.flume.interceptor.utils.JSONUtil.isJSONValidate(JSONUtil.java:8)
at com.studybigdata.flume.interceptor.ETLInterceptor.intercept(ETLInterceptor.java:28)
at com.studybigdata.flume.interceptor.ETLInterceptor.intercept(ETLInterceptor.java:39)
at org.apache.flume.interceptor.InterceptorChain.intercept(InterceptorChain.java:62)
at org.apache.flume.channel.ChannelProcessor.processEventBatch(ChannelProcessor.java:148)
at org.apache.flume.source.taildir.TaildirSource.tailFileProcess(TaildirSource.java:276)
at org.apache.flume.source.taildir.TaildirSource.process(TaildirSource.java:239)
at org.apache.flume.source.PollableSourceRunner$PollingRunner.run(PollableSourceRunner.java:133)
at java.lang.Thread.run(Thread.java:750)
可以看到com.alibaba.fastjson.JSONException被打印了出来。
消费¶
在Kafka消费时,非法JSON应被Flume过滤,只消费到了合法的JSON数据。
{"id":1,"name":"张三","email":"zhangsan@example.com","isValid":true,"reason":null}
{"id":2,"name":"李四","email":"lisi@example.com","isValid":true,"reason":null}
{"id":3,"name":"王五","email":"wangwu@example.com","isValid":true,"reason":null}
测试通过后,我们便可以使用moc-data.jar模拟数据了。
总结¶
本次项目围绕教育大数据用户浏览行为日志采集展开,核心解决了日志采集过程中不完整JSON脏数据的过滤问题。通过搭建Maven工程,开发Flume ETL拦截器,结合JSON工具类实现脏数据校验,经测试可有效保留完整JSON日志、过滤不完整数据;后续通过打包部署、配置Flume Job,将日志数据通过Kafka Channel传输至指定Topic,完成实时日志的有效采集与流转。整个流程实现了日志采集的净化与高效传输,为后续教育大数据的分析处理提供了高质量的数据源支撑。