2 个版本
0.2.1 | 2020 年 1 月 7 日 |
---|---|
0.2.0 | 2020 年 1 月 7 日 |
0.1.0 |
|
#1462 在 数据库接口
22KB
144 行
audis
Audis 是一个基于 Redis 键值存储解决方案的多索引审计日志实现。
审计日志由零个或多个事件对象组成,每个对象与一个或多个主题相关联。Redis 数据库与审计日志之间是一对一的关系 -- 每个审计日志恰好占用一个 Redis 实例,并且一个实例只能包含一个审计日志。
示例:记录事件
使用 audis 的最简单方法是将它指向一个 Redis 实例并开始记录
extern crate audis;
fn main() {
let client = audis::Client::connect("redis://127.0.0.1:6379").unwrap();
client.log(&audis::Event{
id: "foo1".to_string(),
data: "{\"some\":\"data\"}".to_string(),
subjects: vec![
"system".to_string(),
"user:42".to_string(),
],
}).unwrap();
// ... etc ...
}
检索审计日志
如果不能查看审计日志,那么它还有什么价值呢?
extern crate audis;
fn main() {
let client = audis::Client::connect("redis://127.0.0.1:6379").unwrap();
for subject in &client.subjects().unwrap() {
println!("## {} ######################", subject);
for event in &client.retrieve(subject).unwrap() {
println!(" {}", event.data);
}
println!("");
}
}
通过线程实现分布式审计日志
Audis 的一个常见模式是将单个线程委派给将事件日志推送到 Redis 的任务,这样其他线程就可以专注于自己的工作,而不会被审计层的瞬间中断所拖慢。
您可以通过 background()
函数来完成此操作,该函数返回一个缓冲通道,您可以将事件发送到该通道,以及执行后台线程的连接句柄
extern crate audis;
fn main() {
let client = audis::Client::connect("redis://127.0.0.1:6379").unwrap();
// buffer 50 events
let (tx, thread) = client.background(50).unwrap();
tx.send(audis::Event{
id: "foo1".to_string(),
data: "{\"some\":\"data\"}".to_string(),
subjects: vec![
"system".to_string(),
"user:42".to_string(),
],
}).unwrap();
// ... etc ...
thread.join().unwrap();
}
实现细节
Audis 使用四种(4)种类型的对象:A) 事件本身,B) 插入事件的引用计数,C) 每个主题的事件列表,按插入顺序排序,以及 D) 所有已知主题的主列表。
每个审计事件都存储为一个不透明的数据块,通常是 JSON -- 对于这个库来说,事件的精确内容并不重要。每个事件都拥有自己的 Redis 键,该键由其全局唯一 ID 导出,形式为 audit:$id
。这使得检索事件成为使用 Redis GET
命令的 O(1) 操作。
每个事件对象都伴随着一个引用计数,该计数在一个并行键结构中保持,该结构将 :ref
添加到主事件对象键。这些引用计数是整数,用于跟踪有多少不同的主题目前正在引用给定的事件。例如,audit:ae2:ref
是 audit:ae2
的引用计数键。
审计日志中的每个主题都维护着自己相关的事件ID列表。这些列表存储在从主题本身派生出的键下。强烈建议调用者确保主题名称尽可能唯一,以满足分析需求。
最后,存在一个名为 subjects
的单个Redis集合,用于跟踪所有已知主题字符串的完整集合。这有助于发现审计日志的不同子集。
以下是 LOG(e)
操作的插入逻辑伪代码,其中 e
是一个对象
LOG(e):
var id = $e[id]
SETEX "audit:$id" $e[data]
for s in $e[subjects]:
SADD "subjects" "$s"
RPUSH "$s" "$id"
INCR "audit:$id:ref"
从技术角度来看,LOG(e)
以 O(n) 的线性速度运行,其中 n 是审计日志事件应用到的主题数量。然而,鉴于这个 n 通常非常小(几乎总是 < 100),LOG(e)
的性能良好。
RETR(s)
是直接的:通过 LRANGE
遍历Redis中的主题列表,然后通过 GET
获取引用的事件对象
RETR(s):
var log = []
for id in LRANGE "$s" 0 -1:
$log.append( GET "audit:$id" )
return $log
由于 LOG(e)
只会增加我们的审计日志数据集,而 RETR(s)
是只读操作,我们的Redis占用空间将永远增长,除非我们定义清除旧日志条目的操作。删除审计日志的一部分似乎是错误的,并且是适得其反的——整个目的是要知道发生了什么!然而,存储(尤其是内存)是有限的,对于调试目的(至少),审计日志事件随着时间的推移变得不那么相关。
因此,我们定义了两个修剪操作:TRUNC(s,n)
,用于将主题事件集截断到最近的 n
个审计事件,以及 PURGE(s,last)
,用于从主题事件集中删除事件,直到找到给定的ID。(该ID也将被删除)。
这两个操作允许我们定义一个清理例程,该例程在audis库之外运行,可以在最终从Redis中删除它们之前,将审计事件渲染并持久化到文件系统或外部blobstore(例如S3)中。
以下是 TRUNC(s,n)
的伪代码
TRUNC(s,n):
var end = 0 - n - 1
for id in LRANGE "$s" 0 $end:
LPOP "$s"
DECR "audit:$id:ref"
if GET "audit:$id:ref" <= 0:
DEL "audit:$id:ref"
DEL "audit:$id"
随着事件从主题的索引中截断,将检查相关的引用计数,以确定是否需要执行更大的清理(通过 DEL
)。
PURGE(s,last)
类似
PURGE(s,last):
for id in LRANGE "$s" 0 -1:
LPOP "$s"
DECR "audit:$id:ref"
if GET "audit:$id:ref" <= 0:
DEL "audit:$id:ref"
DEL "audit:$id"
if $id == $last
break
这两个操作在同时与其他操作或彼此调用时都会遇到严重问题。该库的将来版本将通过在同一个Redis数据库中巧妙地使用 LOCK()
/UNLOCK()
基本原语来纠正这一点。
依赖关系
~6MB
~131K SLoC