Chào các bạn đã tới phần 2 của chuỗi bài viết lập trình HTTP tracker. Ở phần này chúng ta sẽ tập trung phần xử lý dữ liệu nhận vào từ phía client.
Ôn bài
Ở phần trước, chúng ta đã tìm hiểu yêu cầu, xác định yêu cầu, khởi tạo project, lập trình phần nhận data trong project. Chúng ta sẽ ôn lại bài một tí về phần yêu cầu:
- Thu thập và đo lường được số lượng đăng ký thành công/thất bại theo device (mobile, desktop), theo browsers (Google Chrome, Firefox…).
- Thống kê theo thời gian hoàn thành đăng ký (từ lúc ấn đăng ký đến khi nhận màn hình thành công/thất bại); nếu thất bại, thống kê thêm nguyên nhân.
- Số lượng người đăng ký là có tài khoản trên website.
Vậy để thỏa mãn yêu cầu, chúng ta sẽ tiếp tục phần xử lý dữ liệu ra được những số liệu cơ bản, phù hợp với yêu cầu.
Tổng quan
Để đáp ứng yêu cầu, chúng ta sẽ cần một bộ cơ sở dữ liệu để lưu trữ các thống kê, cũng như lưu trữ phần log đã được gửi lên, để chúng ta có thể dễ dàng đối soát, chạy lại dữ liệu, phân tích sâu hoặc sử dụng AI/ML. Ngoài ra, theo bạn đánh giá bộ cơ sở dữ liệu này cần đáp ứng lưu trữ theo giờ phù hợp nhu cầu báo và theo bạn đánh giá.
Sau khi đánh giá, Lead của bạn yêu cầu bạn sử dụng Redis để lưu trữ thống kê và log sẽ được write vào 1 file TSV(tab-separated values). Lưu ý: với các bạn có nhiều kinh nghiệm có thể sẽ thấy yêu cầu này hơi vô lý, tuy nhiên bài viết hướng tới mục tiêu đào tạo và hướng dẫn lập trình trong mảng data, nên tác giả sẽ lựa chọn bộ công nghệ phù hợp với người đọc nhầm mang tính chất hướng dẫn.
Viết code
Chúng ta cần gắn thêm thư viện Redis vào project:
dependencies {
...
compile group: 'io.vertx', name: 'vertx-redis-client', version: "${vertxVersion}"
}
Để phục vụ nhu cầu báo cáo theo các metrics theo giờ chúng ta cần sử dụng hash stored của Redis. Dữ liệu sẽ được thiết kế theo cấu trúc như sau:
Chúng ta sẽ bắt đầu thay đổi code ở bài tập trước như sau: thay đổi main method
public static void main(String[] args) {
//Each application should have one vertx instance. We can create multiple threading on one instance later.
Vertx vertx = Vertx.vertx();
//Create a MainStarter object from the class, so we can call the method handleRequest from the object.
MainStarter handlerObj = new MainStarter();
//Create a router from vertx instance.
Router router = Router.router(vertx);
router.post("/accept_tracking")
.handler(BodyHandler.create())
.handler(handlerObj::handleRequest);
//Initialize Redis instance
RedisOptions options = new RedisOptions()
.setConnectionString("redis://localhost:6379");
Future<RedisConnection> connect = Redis.createClient(vertx, options).connect();
connect.onSuccess(res -> {
handlerObj.setRedisAPI(RedisAPI.api(res));
//We need to make sure Redis connection is established successfully before init the HTTP service
HttpServer httpServer = vertx.createHttpServer();
httpServer.requestHandler(router).listen(8080);
});
}
Thêm method setRedisAPI để có thể gọi từ method main:
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.FileSystem;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.redis.client.Redis;
import io.vertx.redis.client.RedisAPI;
import io.vertx.redis.client.RedisConnection;
import io.vertx.redis.client.RedisOptions;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MainStarter {
RedisAPI redisAPI = null;
public void setRedisAPI(RedisAPI redisAPI) {
this.redisAPI = redisAPI;
}
DateFormat hDF = new SimpleDateFormat("yyyy-MM-dd-HH");
DateFormat fileDF = new SimpleDateFormat("'log_'yyyy-MM-dd-HH'.tsv'");
...
Thêm vào function đẩy dữ liệu vào Redis:
private void sumToRedis(Date receiveTime, JsonObject row) {
boolean isSuccess = row.getString("result").equals("success");
String hKey = hDF.format(receiveTime);
if(isSuccess) {
this.redisAPI.hincrby(hKey, "success_device:" + row.getString("device"), "1");
this.redisAPI.hincrby(hKey, "success_browser:" + row.getString("browser"), "1");
this.redisAPI.hincrby(hKey, "success_time", row.getInteger("duration") + "");
if(row.getString("accountId") != null)
this.redisAPI.hincrby(hKey, "success_reg_user", "1");
} else {
this.redisAPI.hincrby(hKey, "error_device:" + row.getString("device"), "1");
this.redisAPI.hincrby(hKey, "error_browser:" + row.getString("browser"), "1");
this.redisAPI.hincrby(hKey, "error_time", row.getInteger("duration") + "");
this.redisAPI.hincrby(hKey, "error_cause:" + row.getString("message"), "1");
if(row.getString("accountId") != null)
this.redisAPI.hincrby(hKey, "error_reg_user", "1");
}
}
Thêm vào function chép dữ liệu vào file TSV:
//Since FileSystem instance need to be created from Vertx instance, we have to put in method
private void writeLog(Vertx vertx, Date receiveTime, JsonObject row) {
FileSystem fs = vertx.fileSystem();
StringBuilder sb = new StringBuilder();
sb.append(row.getString("device")).append("\t");
sb.append(row.getString("browser")).append("\t");
sb.append(row.getString("result")).append("\t");
sb.append(row.getString("message")).append("\t");
sb.append(row.getInteger("duration")).append("\t");
sb.append(row.getString("accountId", ""));
fs.writeFile(fileDF.format(receiveTime), Buffer.buffer(sb.toString()));
}
Update lại phần code trong handleRequest method
public void handleRequest(RoutingContext context) {
//Get response data from context
HttpServerResponse response = context.response();
response.setStatusCode(200).end();
try {
//This will get body as Buffer then cast it to JsonObject
JsonObject dataBody = context.body().asJsonObject();
System.out.println("Data Received: " + dataBody.toString());
//If we let above methods use their own time, it is possible that data will be discrepancy
Date receiveTime = new Date();
this.sumToRedis(receiveTime, dataBody);
this.writeLog(context.vertx(), receiveTime, dataBody);
} catch (Exception e) {
e.printStackTrace();
}
}
Code đã được thay đổi xong, bây giờ chúng ta sẽ chạy thử nhé. Chúng ta sẽ lặp lại thao tác gửi giả lập dữ liệu từ Postman hay command như ở bài trước.
curl --location --request POST 'http://localhost:8080/accept_tracking' \
--header 'Content-Type: application/json' \
--data-raw '{
"device": "mobile",
"browser": "Chrome",
"result": "success",
"message": "OK",
"duration": 3432,
"accountId": null
}'
Dữ liệu được gửi thành công, bạn đã thấy log đã gửi về HTTP tracker server và có file được ghi ra trong file TSV với nội dung như bên dưới.
Data Received: {"device":"mobile","browser":"Chrome","result":"success","message":"OK","duration":3432,"accountId":null}
//In TSV file
mobile Chrome success OK 3432 null
Để kiểm tra trên Redis dữ liệu đã được lưu trữ thành công, bạn có thể sử dụng P3X Redis UI, khi thành công dữ liệu sẽ được lưu trữ tương tự như sau:
Tóm tắt
Ở phần này chúng ta đã biết thêm về:
- Cách sử dụng Redis trên Vertx để lưu trữ key dữ liệu.
- Cách sao lưu dữ liệu vào file dưới dạng TSV bằng Vertx.
Các bạn có thể trực tiếp lấy git source của bài viết ở đây Github. Ở phần tiếp theo chúng ta sẽ tối ưu hóa một chút về sao lưu dữ liệu trên Redis và cách sắp xếp dữ liệu trên file theo thời gian trong một dự án thực tế.
Hẹn gặp các bạn ở bài sau.