#audit #log #redis #events #built #subject #atop

audis

基于 Redis 构建的审计日志系统

2 个版本

0.2.1 2020 年 1 月 7 日
0.2.0 2020 年 1 月 7 日
0.1.0 2020 年 1 月 7 日

#1462数据库接口

MIT 协议

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:refaudit: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