首页
/ Coolify 中的 Laravel Nightwatch 观测配置实战:采样、过滤与脱敏三阶段策略

Coolify 中的 Laravel Nightwatch 观测配置实战:采样、过滤与脱敏三阶段策略

2026-09-06 10:34:27作者:何举烈Damon

本文围绕 Coolify 仓库中的 Nightwatch 配置技能文档(SKILL.md)展开,系统讲解 Laravel Nightwatch 的三阶段事件采集模型——采样(Sampling)、过滤(Filtering)、脱敏(Redaction),并覆盖全部九种事件类型的配置参数、环境变量与代码示例。读完后,你将能够在生产环境按流量规模调整采样率、精准过滤噪音事件、对 PII 与凭据做脱敏,并结合 Coolify 仓库中 s6-overlay 的 agent 部署方式,把 Nightwatch 完整地跑起来。

Nightwatch 是什么,Coolify 如何引入它

Laravel Nightwatch 是 Laravel 生态中的开发环境可观测性工具,用于采集请求、命令、数据库查询、缓存操作、队列任务、邮件、通知、出站 HTTP 请求和异常等运行时事件。Coolify 在其 Laravel 12 应用中引入了该包:

  • composer.json 中声明依赖为 "laravel/nightwatch": "^1.28.6"AGENTS.md 的生态清单中确认 laravel/nightwatch (NIGHTWATCH) - v1,运行环境为 PHP 8.5;
  • boost.jsonconfigure-nightwatch 注册为项目的 Agent 技能之一(同时 nightwatch_mcp: false,即未启用其 MCP 扩展),说明该配置知识以技能文档形式随仓库分发;
  • 技能文档自身(SKILL.md)也声明:Nightwatch 官方文档是所有配置项的最终权威来源,本文档提供的是实践指导与常见模式,具体环境变量和 API 行为以官方文档为准。

事件采集的三阶段模型

Nightwatch 对事件的处理分为三个明确阶段,理解这个模型是后面所有配置的前提:

  1. 采样(Sampling)——决定哪些"入口"(请求、命令、定时任务)触发完整 trace 采集。采样命中后,该入口产生的所有关联事件都会被记录。
  2. 过滤(Filtering)——在采样之后,把特定事件(查询、缓存、邮件等)从采集中剔除,用于降噪和节省配额。
  3. 脱敏(Redaction)——事件保留,但对其内容做修改,移除或混淆敏感信息。

文档给出的数据流如下:

Request/Command/Scheduled Task
       |
       v
   [Sampling?] ----NO----> Drop entire trace
       | YES
       v
   Events generated
       |
       v
   [Filtering?] ----YES---> Drop specific event
       | NO
       v
   [Redaction] ----------> Store modified data

三个阶段的关键区别在于作用粒度:采样是"全有或全无"(miss 掉就丢掉整条 trace),过滤是"逐事件丢弃",脱敏则是"事件保留、内容净化"。

采样配置(Sampling)

采样决定哪些入口触发完整 trace 采集。技能文档建议:生产环境请求采样从 0.1(10%)起步,再根据流量与需求调整。

全局采样率(环境变量)

# 默认:100% 采样(所有请求/命令都被采集)

NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1      # 推荐:10% 的请求
NIGHTWATCH_COMMAND_SAMPLE_RATE=1.0      # 采集全部命令
NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0    # 异常永远采集

三个采样率相互独立:请求、命令、异常各自一个开关,其中异常建议保持 1.0,保证排查问题时信息不缺失。

路由级采样(Route-Based Sampling)

通过 Sample 中间件对特定路由应用不同采样率:

use Illuminate\Support\Facades\Route;
use Laravel\Nightwatch\Http\Middleware\Sample;

// 管理后台路由 100% 采样
Route::middleware(Sample::rate(1.0))->prefix('admin')->group(function () {
    // All admin routes sampled fully
});

// API 路由 5% 采样
Route::middleware(Sample::rate(0.05))->prefix('api')->group(function () {
    // API routes sampled sparingly
});

// 关键端点永远采样
Route::post('/checkout', [CheckoutController::class, 'process'])
    ->middleware(Sample::always());

// 健康检查永不采样
Route::get('/health', [HealthController::class, 'check'])
    ->middleware(Sample::never());

Sample::rate()Sample::always()Sample::never() 三个静态方法覆盖了"按比例 / 全量 / 排除"三种典型诉求。Coolify 自身的路由文件(routes/web.phproutes/api.php)中目前未使用这些中间件,上述示例是文档给出的标准用法。

未匹配路由的采样

404 与爬虫流量往往量很大且价值低,建议用 fallback 路由单独压低采样率:

Route::fallback(fn () => abort(404))
    ->middleware(Sample::rate(0.01));  // 未匹配路由只采样 1%

动态采样

基于运行时条件(用户角色、请求属性)动态决定采样:

use Closure;
use Illuminate\Http\Request;
use Laravel\Nightwatch\Facades\Nightwatch;

class SampleAdminRequests
{
    public function handle(Request $request, Closure $next)
    {
        if ($request->user()?->isAdmin()) {
            Nightwatch::sample();  // 管理用户的请求永远采样
        }
        return $next($request);
    }
}

即在前置中间件里调用 Nightwatch::sample() 强制本次请求进入采样,实现"按身份"而非"按路由"的观测策略。

命令采样与排除特定命令

通过监听 CommandStarting 事件排除不需要观测的命令(如 schedule:finishhorizon:snapshot 这类框架周期性任务):

use Illuminate\Console\Events\CommandStarting;
use Illuminate\Support\Facades\Event;
use Laravel\Nightwatch\Facades\Nightwatch;

public function boot(): void
{
    Event::listen(function (CommandStarting $event) {
        if (in_array($event->command, ['schedule:finish', 'horizon:snapshot'])) {
            Nightwatch::dontSample();
        }
    });
}

Vendor 命令

Nightwatch 默认忽略框架/内部(vendor)命令以减少噪音。如果确实需要采集,可显式开启:

Nightwatch::captureDefaultVendorCommands();

过滤配置(Filtering)

过滤用于在采样命中之后剔除特定事件,降低噪音与配额消耗。每类事件都提供"一刀切环境变量"和"程序化回调"两种手段。

数据库查询

全部禁用查询采集:

NIGHTWATCH_IGNORE_QUERIES=true

按 SQL 模式过滤特定查询:

use Laravel\Nightwatch\Facades\Nightwatch;
use Laravel\Nightwatch\Records\Query;

public function boot(): void
{
    // 过滤 job 表查询(PostgreSQL 风格)
    Nightwatch::rejectQueries(function (Query $query) {
        return str_contains($query->sql, 'into "jobs"');
    });

    // 过滤缓存表查询(MySQL 风格)
    Nightwatch::rejectQueries(function (Query $query) {
        return str_contains($query->sql, 'from `cache`')
            || str_contains($query->sql, 'into `cache`');
    });
}

回调接收 Laravel\Nightwatch\Records\Query 记录,判断其 sql 属性即可。

缓存事件

全部禁用:

NIGHTWATCH_IGNORE_CACHE_EVENTS=true

按键模式过滤,支持精确匹配与正则(以 / 包裹的字符串视为正则):

Nightwatch::rejectCacheKeys([
    'my-app:users',                    // 精确匹配
    '/^my-app:posts:/',                // 正则:以 my-app:posts: 开头
    '/^[a-zA-Z0-9]{40}$/',             // 正则:会话 ID 形态
]);

或用回调对完整缓存事件做判断:

use Laravel\Nightwatch\Records\CacheEvent;

Nightwatch::rejectCacheEvents(function (CacheEvent $cacheEvent) {
    return str_starts_with($cacheEvent->key, 'temp:');
});

邮件事件

全部禁用:

NIGHTWATCH_IGNORE_MAIL=true

按主题过滤特定邮件:

use Laravel\Nightwatch\Records\Mail;

Nightwatch::rejectMail(function (Mail $mail) {
    return str_contains($mail->subject, 'Newsletter');
});

通知事件

全部禁用:

NIGHTWATCH_IGNORE_NOTIFICATIONS=true

按通知渠道过滤(例如只排除 database 渠道的站内通知):

use Laravel\Nightwatch\Records\Notification;

Nightwatch::rejectNotifications(function (Notification $notification) {
    return $notification->channel === 'database';
});

出站 HTTP 请求

全部禁用:

NIGHTWATCH_IGNORE_OUTGOING_REQUESTS=true

按目标 URL 过滤(例如排除对 analytics 服务的调用):

use Laravel\Nightwatch\Records\OutgoingRequest;

Nightwatch::rejectOutgoingRequests(function (OutgoingRequest $request) {
    return str_contains($request->url, 'analytics.example.com');
});

队列任务

按任务类名过滤特定 Job:

use Laravel\Nightwatch\Records\QueuedJob;

Nightwatch::rejectQueuedJobs(function (QueuedJob $job) {
    return $job->name === 'App\Jobs\LowPriorityJob';
});

解耦 Job 采样(Decoupling Job Sampling)

队列任务天然运行在与产生它的请求不同的进程/上下文中,可以把 Job 的采样率与父上下文解耦,通过 Queue::before 钩子独立设定:

use Illuminate\Support\Facades\Queue;

public function boot(): void
{
    Queue::before(fn () => Nightwatch::sample(rate: 0.5));
}

即每次 Job 执行前以 50% 概率采样,不受原请求是否被采样的影响。对 Coolify 这类大量使用队列的系统(其 app/Jobs 下定义了上百个 Job),该机制可以让后台任务链路的观测覆盖不再依赖入口请求的采样命中。

脱敏配置(Redaction)

脱敏与过滤的本质区别:事件仍然被保留(trace 里能看到"发生了什么"),但敏感内容被替换或混淆,从而兼顾可观测性与隐私。

请求脱敏

请求头默认自动脱敏 AuthorizationCookieX-XSRF-TOKEN;可用环境变量自定义脱敏头清单:

# 自定义需要脱敏的请求头
NIGHTWATCH_REDACT_HEADERS=Authorization,Cookie,Proxy-Authorization,X-API-Key

请求体(payload)采集默认关闭,显式开启后需同步配置脱敏字段:

# 开启 payload 采集
NIGHTWATCH_CAPTURE_REQUEST_PAYLOAD=true

# 自定义需脱敏的 payload 字段
NIGHTWATCH_REDACT_PAYLOAD_FIELDS=password,password_confirmation,ssn,credit_card

程序化脱敏可以处理 URL、IP 等结构化属性:

use Laravel\Nightwatch\Facades\Nightwatch;
use Laravel\Nightwatch\Records\Request;

Nightwatch::redactRequests(function (Request $request) {
    $request->url = str_replace('secret', '***', $request->url);
    $request->ip = preg_replace('/\d+$/', '***', $request->ip);
});

其他事件类型的脱敏

查询(改写 SQL 文本):

use Laravel\Nightwatch\Records\Query;

Nightwatch::redactQueries(function (Query $query) {
    $query->sql = str_replace('secret_token', '***', $query->sql);
});

缓存(改写键名):

use Laravel\Nightwatch\Records\CacheEvent;

Nightwatch::redactCacheEvents(function (CacheEvent $cacheEvent) {
    $cacheEvent->key = str_replace('user:', 'user:***:', $cacheEvent->key);
});

命令(掩盖命令行中的敏感参数):

use Laravel\Nightwatch\Records\Command;

Nightwatch::redactCommands(function (Command $command) {
    $command->command = preg_replace('/--password=\S+/', '--password=***', $command->command);
});

异常(净化异常消息):

use Laravel\Nightwatch\Records\Exception;

Nightwatch::redactExceptions(function (Exception $exception) {
    $exception->message = str_replace('secret', '***', $exception->message);
});

邮件(改写主题):

use Laravel\Nightwatch\Records\Mail;

Nightwatch::redactMail(function (Mail $mail) {
    $mail->subject = str_replace('Invoice #', 'Invoice ***', $mail->subject);
});

出站请求(净化 URL 中的查询参数):

use Laravel\Nightwatch\Records\OutgoingRequest;

Nightwatch::redactOutgoingRequests(function (OutgoingRequest $outgoingRequest) {
    $outgoingRequest->url = preg_replace('/api_key=\w+/', 'api_key=***', $outgoingRequest->url);
});

各事件类型配置速查

技能配套的速查表(reference.md)汇总了每类事件的采样、过滤、脱敏三个维度的可用手段:

事件类型 采样 过滤 脱敏
Requests NIGHTWATCH_REQUEST_SAMPLE_RATE、路由中间件 不适用 请求头、payload、URL、IP
Commands NIGHTWATCH_COMMAND_SAMPLE_RATE、事件监听 不适用 命令参数
Queries 随父上下文 rejectQueries()NIGHTWATCH_IGNORE_QUERIES SQL 语句
Cache 随父上下文 rejectCacheKeys()rejectCacheEvents()NIGHTWATCH_IGNORE_CACHE_EVENTS 缓存键
Jobs 随父上下文、Queue::before rejectQueuedJobs() 不适用
Mail 随父上下文 rejectMail()NIGHTWATCH_IGNORE_MAIL 邮件主题
Notifications 随父上下文 rejectNotifications()NIGHTWATCH_IGNORE_NOTIFICATIONS 不适用
Outgoing Requests 随父上下文 rejectOutgoingRequests()NIGHTWATCH_IGNORE_OUTGOING_REQUESTS URL
Exceptions NIGHTWATCH_EXCEPTION_SAMPLE_RATE 不适用 异常消息

从这张表可以读出三阶段模型的完整映射:只有入口型事件(请求、命令、异常)拥有独立采样开关,其余派生事件(查询、缓存、邮件等)都"随父上下文"——父 trace 被采样才谈得上采集它们,因此"压低请求采样率"是控制数据总量的第一杠杆,"过滤 + 脱敏"则是精细化的第二、三杠杆。

生产环境预设方案

reference.md 给出三套可直接套用的生产预设:

高流量应用(保守采样 + 激进的噪音过滤)

# 保守采样
NIGHTWATCH_REQUEST_SAMPLE_RATE=0.01          # 1% 请求
NIGHTWATCH_COMMAND_SAMPLE_RATE=0.1          # 10% 命令
NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0        # 异常永远采集

# 过滤噪音事件
NIGHTWATCH_IGNORE_CACHE_EVENTS=true
NIGHTWATCH_IGNORE_QUERIES=true               # 或改用程序化方式过滤特定查询

隐私优先应用(禁用敏感数据 + 强化头脱敏)

# 禁用敏感数据采集
NIGHTWATCH_CAPTURE_REQUEST_PAYLOAD=false
NIGHTWATCH_REDACT_HEADERS=Authorization,Cookie,Proxy-Authorization,X-XSRF-TOKEN

# 或在 AppServiceProvider 中做程序化脱敏

均衡配置(推荐起点)

# 采样率
NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1
NIGHTWATCH_COMMAND_SAMPLE_RATE=1.0
NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0

# 程序化过滤明显的噪音事件
# 按需对 PII 做脱敏

配置完成后的验证清单

按 reference.md 的清单逐项确认:

  • [ ] 采样率与流量规模匹配(高流量用 0.01,常规 0.1 起步);
  • [ ] 噪音事件已过滤(缓存事件、特定查询表);
  • [ ] 敏感数据已脱敏(PII、token、凭据);
  • [ ] 异常保持 100% 采集,保障排障能力;
  • [ ] 在开发环境用 NIGHTWATCH_REQUEST_SAMPLE_RATE=1.0 验证全量链路可用;
  • [ ] 在 Nightwatch 面板持续观察事件配额用量。

常见组合模式

reference.md 还沉淀了三个高频组合场景:

1. 过滤健康检查 + 压低采样——探针类端点既无观测价值又高频:

Route::get('/health', fn() => ['status' => 'ok'])
    ->middleware(Sample::never());

2. 排除内部观测框架自身的查询——避免"观测工具观测自己"造成的递归噪音(例如 telescope、pulse 表的读写):

Nightwatch::rejectQueries(fn($q) =>
    str_contains($q->sql, 'telescope') ||
    str_contains($q->sql, 'pulse')
);

3. 保护缓存键中的用户数据——用户 ID 出现在缓存键里时做脱敏而非过滤,保住"哪个键被访问"的结构信息:

Nightwatch::redactCacheEvents(fn($e) =>
    $e->key = preg_replace('/user:\d+/', 'user:***', $e->key)
);

Coolify 仓库中的 Nightwatch 落地方式

以上配置项在 Coolify 仓库里的实际接入方式,可以从源码与部署脚本中得到印证:

1. 总开关默认关闭。 config/constants.php 定义了 NIGHTWATCH_ENABLED 的读取,且默认值为 false

'nightwatch' => [
    'is_nightwatch_enabled' => env('NIGHTWATCH_ENABLED', false),
],

即 Nightwatch 在 Coolify 中是可选能力,不配置 NIGHTWATCH_ENABLED=true 时完全不启用。

2. 生产镜像通过 s6-overlay 托管 agent 进程。 docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run 的启动脚本逻辑是:

cd /var/www/html

if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then
    echo "   INFO  Nightwatch is enabled, starting..."
    exec php artisan nightwatch:agent
fi

echo "   INFO  Nightwatch is disabled, sleeping."
exec sleep infinity

即容器内该服务始终存在,但只有 .env 中显式 NIGHTWATCH_ENABLED=true 时才真正执行 php artisan nightwatch:agent,否则进程进入 sleep infinity 空转——与上一条配置默认值形成呼应。该服务在 s6 的依赖拓扑中被列为 user 服务组成员(见 docker/production/etc/s6-overlay/s6-rc.d/user/contents.d 下的 nightwatch-agent 条目),与 horizonscheduler-worker 等并列。

3. 开发镜像额外做容器角色隔离。 开发环境的同名脚本(docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run)在检查环境变量之前,还先通过 coolify_container_has_role 判断当前容器的 COOLIFY_CONTAINER_ROLE 是否包含 workernightwatchnightwatch-agent 角色,不满足则直接 sleep,避免多容器开发拓扑中每个容器都起一个 agent。对应测试 tests/Feature/ContainerRoleScriptTest.php 验证了 'horizon,scheduler,nightwatch,flux' 这类逗号分隔角色串能正确匹配 nightwatch 角色,并断言生产镜像的三个服务脚本(horizon、nightwatch-agent、scheduler-worker)中不含角色辅助逻辑——即角色隔离只存在于开发镜像,保持生产镜像精简。

4. 技能文档的 Agent 化分发。 boost.jsonconfigure-nightwatch 列入 skills 清单,意味着在 Coolify 仓库内工作时,编码 Agent 会自动激活这份配置技能来指导 Nightwatch 相关变更;技能文档本身(SKILL.mdreference.md)就是本文所依据的全部配置知识。

小结

Nightwatch 的调优本质上是在"观测价值 / 数据成本 / 隐私合规"三者之间做分层决策:入口层用采样率控制 trace 总量(请求、命令、异常三个独立开关 + 路由/中间件/动态采样细化),事件层用 reject*() 回调和 NIGHTWATCH_IGNORE_* 环境变量剔除噪音,内容层用 redact*() 回调和环境变量净化 PII 与凭据。Coolify 仓库则以 NIGHTWATCH_ENABLED(默认 false)+ s6-overlay 常驻服务 + 开发镜像角色隔离的方式,把整套能力做成可选、可验证、可回退的部署形态——这也是自托管 PaaS 接入开发环境可观测性时值得参考的工程模式。

登录后查看全文
热门项目推荐
相关项目推荐