{"id":18602776,"url":"https://github.com/obgnail/audit-log","last_synced_at":"2026-04-30T09:31:22.149Z","repository":{"id":65935520,"uuid":"515515733","full_name":"obgnail/audit-log","owner":"obgnail","description":"高解耦的MySQL操作日志框架","archived":false,"fork":false,"pushed_at":"2023-02-09T16:05:31.000Z","size":437,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-18T01:41:56.607Z","etag":null,"topics":["audit","audit-log","audit-logs","binlog","clickhouse","golang","gtid","kafka","mysql","mysql-database","sync"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/obgnail.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":"audit_log/audit_log.go","citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-19T09:18:42.000Z","updated_at":"2023-03-15T15:14:43.000Z","dependencies_parsed_at":"2023-05-15T05:30:18.343Z","dependency_job_id":null,"html_url":"https://github.com/obgnail/audit-log","commit_stats":{"total_commits":11,"total_committers":1,"mean_commits":11.0,"dds":0.0,"last_synced_commit":"59d64682d72336fdaac56e974505d1b43995958d"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obgnail%2Faudit-log","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obgnail%2Faudit-log/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obgnail%2Faudit-log/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obgnail%2Faudit-log/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/obgnail","download_url":"https://codeload.github.com/obgnail/audit-log/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254582909,"owners_count":22095519,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audit","audit-log","audit-logs","binlog","clickhouse","golang","gtid","kafka","mysql","mysql-database","sync"],"created_at":"2024-11-07T02:12:29.736Z","updated_at":"2026-04-30T09:31:22.118Z","avatar_url":"https://github.com/obgnail.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Audit log\n\n## 前置知识\n\nMySQL binlog：[MySQL :: MySQL Internals Manual :: 20 The Binary Log](https://dev.mysql.com/doc/internals/en/binary-log.html)\n\nGTID：[MySQL :: MySQL 5.6 Reference Manual :: 17.1.3.1 GTID Concepts](https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html)\n\n\n\n## 架构\n\n![audit-log](assets/audit-log.png)\n\n主要用到的组件有：\n\n- River / Canal：消费 MySQL 的 binlog。组成 binlog_event steam。\n- Binlog Broker：接收 binlog_event steam，进行数据清理，并将其写入 Kafka。\n- TxInfo Broker：监听业务代码产生的事务信息（tx_info），并将其写入 Kafka。\n- Kafka：主要用于存储 MySQL 的 binlog 以及业务代码产生的事务信息（tx_info）。有两个 topic，分别是 binlog 和 tx_info。\n- Binlog Syncer：消费存入 Kafka 中的 binlog，将这些数据存入 ClickHouse。\n- TxInfo Syncder：消费存入 Kafka 中的 tx_info，然后结合已经存入 ClickHouse 的 binlog_event，生成审计日志数据（audit_log）。存入 ClickHouse。\n- ClickHouse：主要提供事务信息（tx_info）、MySQL 二进制文件（binlog）、审计日志数据（audit_log）的存储和查询。\n\n\n\n## 原理\n\n生成审计日志用到的主要方法是**异步监听 MySQL binlog 以及业务代码中对 MySQL 事务 GTID 作标记。**\n\n主要流程如下：\n\n1. 进行业务操作，完成 MySQL 事务提交。\n\n2. 完成事务提交后，TxInfo Broker 会向 Kafka 中发送一条事务消息 tx_info，这个 tx_info 包含的数据有事务的 GTID、具体的操作类型（对应具体的业务操作）、该操作所对应的自定义上下文信息（Context）以及操作的时间 (tx_time)。于此同时 Binlog Broker 会消费 MySQL 的 binlog 并将其写入 Kafka 的 binlog Topic 中。\n\n   ```go\n   type TxInfo struct {\n   \tTime    int64  `db:\"time\" json:\"time\"`\n   \tContext string `db:\"context\" json:\"context\"`\n   \tGTID    string `db:\"gtid\" json:\"gtid\"`\n   }\n   ```\n\n3. Binlog Syncer 会监听消费 Kafka 的 binlog Topic 中的数据，写入 ClickHouse。为之后生成最终的审计日志数据做一些数据准备；binlog_event 的数据为一个事务操作所涉及到的所有数据表发生变更的数据行，后续会通过 tx_info 的 GTID 在 binlog_event 中找出这个事务所影响到的所有数据生成最终的审计日志。\n\n   ```go\n   type BinlogEvent struct {\n   \tDb     string       `json:\"db\"`\n   \tTable  string       `json:\"table\"`\n   \tAction Action       `json:\"action\"`\n   \tGTID   string       `json:\"gtid\"`\n   \tTime   int64        `json:\"time\"`\n   \tData   sql.RawBytes `json:\"data\"`\n   }\n   ```\n\n4. TxInfo Syncer 会监听消费 Kafka 的 tx_info Topic。当 TxInfo Syncer 拿到一条 tx_info 消息后，会通过 GTID 查出所有的 binlog_event，生成最终的审计日志 audit_log，写入 ClickHouse。\n\n5. 业务代码中查询 ClickHouse 的 audit_log 数据进行个性化处理。\n\n\n\n## 使用\n\n1. 初始化配置\n2. 监听审计日志，执行自定义的操作函数\n3. 执行 sql\n\n```go\nimport (\n\t\"fmt\"\n\t\"github.com/juju/errors\"\n\t\"github.com/obgnail/audit-log/audit_log\"\n\t\"github.com/obgnail/audit-log/context\"\n\t\"github.com/obgnail/audit-log/mysql\"\n\t\"github.com/obgnail/audit-log/mysql/utils/uuid\"\n\t\"github.com/obgnail/audit-log/types\"\n\t\"gopkg.in/gorp.v1\"\n\t\"time\"\n)\n\nfunc main() {\n\t// 1. 初始化配置\n\taudit_log.Init(\"./config/config.toml\")\n\n\t// 2. 监听审计日志, 执行自定义的操作函数\n\taudit_log.Run(audit_log.FunctionHandler(func(auditLog *types.AuditLog) error {\n\t\tfmt.Printf(\"get audit log: %+v\\n\", *auditLog)\n\t\treturn nil\n\t}))\n\n\ttime.Sleep(time.Second * 3)\n\n\t// 3. 执行sql\n\tinsertUser()\n\n\tforever := make(chan struct{})\n\t\u003c-forever\n}\n\nfunc insertUser() {\n\t// 每个具体业务对应一个Context。以【插入用户】为例\n\t// Context 中包含业务代码兴趣的环境信息\n\tinsertUserContext := 1\n\tmyContext := context.New(insertUserContext, \"ContextParam1\", \"ContextParam2\")\n\n\t// 执行MySQL事务,携带上面的Context\n\terr := mysql.DBMTransact(myContext.String(), func(tx *gorp.Transaction) error {\n\t\t_uuid := uuid.UUID()\n\t\tuser := \u0026User{_uuid, _uuid + \"Name\", _uuid + \"@gmail.com\", 0}\n\t\tsql := \"INSERT INTO `user` (uuid, name, email, status) VALUES (?, ?, ?, ?);\"\n\t\targs, _ := mysql.BuildSqlArgs(user.UUID, user.Name, user.Email, user.Status)\n\t\tif _, err := tx.Exec(sql, args...); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\treturn nil\n\t})\n\tcheckErr(err)\n}\n\n// output:\n// get audit log: {Time:2023-02-09 21:34:39 +0800 CST Context:1.ContextParam1.ContextParam2 GTID:577b1aef-a03e-11eb-b217-0242ac110003:248 BinlogEvents:[{Db:testdb01 Table:user Action:0 GTID:577b1aef-a03e-11eb-b217-0242ac110003:248 Data:{\"before\":{},\"after\":{\"email\":\"2h1ooBWg@gmail.com\",\"name\":\"2h1ooBWgName\",\"status\":0,\"uuid\":\"2h1ooBWg\"}}}]}\n```\n\n\n\n## Q\u0026A \n\nQ：binlog_event 中的数据会越来越多，久而久之会不会带来非常大的存储开销？\n\nA：binlog_event 中只存储近 30 天的数据，30 天前的数据会自动被删除。因为 binlog_event 中的数据只要被 TxInfo Syncer 使用后就已经失去了存储的意义，因为它永远不会再被任何场景用到。另外如果 binlog_event 如果过了 30 天还没被使用到，那么也可以判定这个数据永远不会再被用到了。\n\n```sql\n-- 在 ClickHouse 中 binlog_event 表的定义\nCREATE TABLE binlog_event\n(\n    `db`     String,\n    `table`  String,\n    `action` Int32,\n    `gtid`   String,\n    `data`   String,\n    `time`   DateTime DEFAULT now()\n) ENGINE = MergeTree()\n      PARTITION BY toYYYYMMDD(time) ORDER BY gtid\n      TTL time + INTERVAL 30 DAY;\n```\n\nbinlog_event 的数据不会被用到会有以下两种情况：\n\n1. binlog_event 确实会存储一些永远不会被用到的数据。比如直接操作 MySQL 进行一些数据修改或新增，这时由于 MySQL 会产生 binlog，所以会生成 binlog_event 数据存储下来，但这个操作由于不是在业务系统中触发的，不会产生 tx_info，所以这个操作也不会被审计，自然存储在 binlog_event 中数据也永远不会用到。\n2. TxInfo Syncer 服务不可用或消息消费出现了超过 30 天的延迟。无论服务不可用还是非常高的延迟都是不能接受的，当出现这类问题时应当及时定位修复，可以将这个 30 天视为我们故障的定位修复时间。\n\n\n\nQ: TxInfo Syncer 是怎样通过 tx_info 的 GTID 来判断是否拿到了这个事务的完整的 binlog_event 的？\n\nA: 主要有两种方式：\n\n1. 会在 TxInfo Syncer 中定义一个 mapCtxBinlogEventCount，主要用来定义某个 Context 需要完整的 binlog_event 数目，这个数一般小于实际的数。通过 tx_info 的 Context 找出需要的数目，通过 tx_info 的 GTID 找出 binlog_event，当找到的数量大于或等于这个数时即可视为找到了完整的 binlog_event。\n2. 如果某个 Context 没有在 mapCtxBinlogEventCount 中定义对应的数目，那么 TxInfo Syncer 会通过 GTID 每隔 100ms 查一次 binlog_event，连续查三次，如果三次查询的结果数目都是一样的，则可视为已经拿到了完整的 binlig_event，如果不一样就继续往复，直到三次结果都一样。\n\n\n\nQ：TxInfo Syncer 通过 tx_info 的 GTID 没有及时查到需要的 binlog_event 会一直卡住吗？\n\nA: 不会。因为 TxInfo Syncer 会通过 GTID 每隔 100ms 查一次 binlog_event，如果查询了 100 次（即 10s）仍然为 0，那么会将这个 tx_info 标记为未完成存入 ClickHouse 的 tx_info 表中，然后继续消费下一条 tx_info。TxInfo Syncer 会另外起一个 goroutine 每隔 10s 将过去 72 小时内未完成的 tx_info 重新进行消费。\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobgnail%2Faudit-log","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fobgnail%2Faudit-log","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobgnail%2Faudit-log/lists"}