mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
refactor(realtime): split error reporting — span for per-action, logger for ad-hoc
Spans model per-action operations (open/message/close); they're a poor fit for ad-hoc events with no operation lifecycle. Split logError to dispatch by whether a span is active: - Active span (realtime.open / realtime.message / realtime.close catches) -> attach the error to the existing span; the Sentry span exporter ships it with full operation context (attributes, duration, trace_id). - No active span (pub/sub subscriber, onStart, Swoole error handler, updateWorkerDocument) -> push a utopia/logger Log via the realtimeLogger registry. Goes to Sentry as an event via the Sentry logger adapter (or to logOwl / Raygun / AppSignal). Same dedicated Realtime project either way. Restores the realtimeLogger registry (dropped in the previous "spans only" pass), inlines the now-single-caller $createLogger closure into the logger registry, and drops the recordRealtimeErrorSpan helper — logError is the only function on this path now. Also registers a Pretty span exporter in app/init/realtime/span.php for non-self-hosted editions so Realtime spans are visible in the container's stdout (on self-hosted the existing app/init/span.php already provides it; gating avoids duplicate output).
This commit is contained in:
@@ -37,3 +37,14 @@ try {
|
||||
} catch (Throwable $error) {
|
||||
Console::warning('Failed to register Realtime Sentry span exporter: ' . $error->getMessage());
|
||||
}
|
||||
|
||||
// Print spans to stdout for local visibility on editions where app/init/span.php — which already
|
||||
// installs a Pretty exporter — isn't loaded (it's gated on `_APP_EDITION === 'self-hosted'`).
|
||||
if (System::getEnv('_APP_EDITION', 'self-hosted') !== 'self-hosted') {
|
||||
Span::addExporter(new Exporter\Pretty(), function (Span $span): bool {
|
||||
if (\str_starts_with($span->getAction(), 'listener.')) {
|
||||
return $span->getError() !== null;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
+62
-20
@@ -46,21 +46,20 @@ if (!Http::isProduction()) {
|
||||
PublicDomain::allow(['request-catcher-webhook']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a utopia/logger Logger from a logging DSN, or null when the config is empty/invalid.
|
||||
* `$legacyProviderName` supplies the provider name for the pre-1.5.x `;`-delimited config format.
|
||||
*/
|
||||
$createLogger = static function (string $providerConfig, string $legacyProviderName = ''): ?Logger {
|
||||
$register->set('logger', function () {
|
||||
// Register error logger
|
||||
$providerName = System::getEnv('_APP_LOGGING_PROVIDER', '');
|
||||
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG', '');
|
||||
|
||||
if (empty($providerConfig)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
$providerName = $legacyProviderName;
|
||||
try {
|
||||
$loggingProvider = new DSN($providerConfig);
|
||||
|
||||
$providerName = $loggingProvider->getScheme();
|
||||
$config = match ($providerName) {
|
||||
$providerConfig = match ($providerName) {
|
||||
'sentry' => ['key' => $loggingProvider->getPassword(), 'projectId' => $loggingProvider->getUser() ?? '', 'host' => 'https://' . $loggingProvider->getHost()],
|
||||
'logowl' => ['ticket' => $loggingProvider->getUser() ?? '', 'host' => $loggingProvider->getHost()],
|
||||
default => ['key' => $loggingProvider->getHost()],
|
||||
@@ -70,7 +69,7 @@ $createLogger = static function (string $providerConfig, string $legacyProviderN
|
||||
Console::warning('Using deprecated logging configuration. Please update your configuration to use DSN format.' . $th->getMessage());
|
||||
$configChunks = \explode(';', $providerConfig);
|
||||
|
||||
$config = match ($providerName) {
|
||||
$providerConfig = match ($providerName) {
|
||||
'sentry' => ['key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => ''],
|
||||
'logowl' => ['ticket' => $configChunks[0], 'host' => ''],
|
||||
default => ['key' => $providerConfig],
|
||||
@@ -78,7 +77,7 @@ $createLogger = static function (string $providerConfig, string $legacyProviderN
|
||||
}
|
||||
|
||||
if (empty($providerName)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Logger::hasProvider($providerName)) {
|
||||
@@ -87,10 +86,10 @@ $createLogger = static function (string $providerConfig, string $legacyProviderN
|
||||
|
||||
try {
|
||||
$adapter = match ($providerName) {
|
||||
'sentry' => new Sentry($config['projectId'], $config['key'], $config['host']),
|
||||
'logowl' => new LogOwl($config['ticket'], $config['host']),
|
||||
'raygun' => new Raygun($config['key']),
|
||||
'appsignal' => new AppSignal($config['key']),
|
||||
'sentry' => new Sentry($providerConfig['projectId'], $providerConfig['key'], $providerConfig['host']),
|
||||
'logowl' => new LogOwl($providerConfig['ticket'], $providerConfig['host']),
|
||||
'raygun' => new Raygun($providerConfig['key']),
|
||||
'appsignal' => new AppSignal($providerConfig['key']),
|
||||
default => null
|
||||
};
|
||||
} catch (Throwable $th) {
|
||||
@@ -99,16 +98,59 @@ $createLogger = static function (string $providerConfig, string $legacyProviderN
|
||||
|
||||
if ($adapter === null) {
|
||||
Console::error('Logging provider not supported. Logging is disabled');
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
return new Logger($adapter);
|
||||
};
|
||||
});
|
||||
|
||||
$register->set('logger', static fn () => $createLogger(
|
||||
System::getEnv('_APP_LOGGING_CONFIG', ''),
|
||||
System::getEnv('_APP_LOGGING_PROVIDER', ''),
|
||||
));
|
||||
$register->set('realtimeLogger', function () {
|
||||
// Realtime falls back to the default logging config when _APP_LOGGING_CONFIG_REALTIME isn't
|
||||
// set. Used by app/realtime.php logError() for ad-hoc errors (pub/sub subscriber, onStart,
|
||||
// worker stats, the Swoole server error handler) — i.e., the call sites that have no active
|
||||
// span. Per-action errors (inside realtime.open / realtime.message / realtime.close) attach to
|
||||
// the active span instead and ship via the Sentry span exporter (app/init/realtime/span.php).
|
||||
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG_REALTIME', '') ?: System::getEnv('_APP_LOGGING_CONFIG', '');
|
||||
|
||||
if (empty($providerConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$loggingProvider = new DSN($providerConfig);
|
||||
$providerName = $loggingProvider->getScheme();
|
||||
$providerConfig = match ($providerName) {
|
||||
'sentry' => ['key' => $loggingProvider->getPassword(), 'projectId' => $loggingProvider->getUser() ?? '', 'host' => 'https://' . $loggingProvider->getHost()],
|
||||
'logowl' => ['ticket' => $loggingProvider->getUser() ?? '', 'host' => $loggingProvider->getHost()],
|
||||
default => ['key' => $loggingProvider->getHost()],
|
||||
};
|
||||
|
||||
if (empty($providerName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Logger::hasProvider($providerName)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Logging provider not supported. Logging is disabled');
|
||||
}
|
||||
|
||||
try {
|
||||
$adapter = match ($providerName) {
|
||||
'sentry' => new Sentry($providerConfig['projectId'], $providerConfig['key'], $providerConfig['host']),
|
||||
'logowl' => new LogOwl($providerConfig['ticket'], $providerConfig['host']),
|
||||
'raygun' => new Raygun($providerConfig['key']),
|
||||
'appsignal' => new AppSignal($providerConfig['key']),
|
||||
default => null
|
||||
};
|
||||
} catch (Throwable $th) {
|
||||
$adapter = null;
|
||||
}
|
||||
|
||||
if ($adapter === null) {
|
||||
Console::error('Logging provider not supported. Logging is disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
return new Logger($adapter);
|
||||
});
|
||||
|
||||
$register->set('pools', function () {
|
||||
$group = new Group();
|
||||
|
||||
+63
-35
@@ -35,6 +35,7 @@ use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Span\Span;
|
||||
@@ -285,46 +286,73 @@ $adapter
|
||||
|
||||
$server = new Server($adapter);
|
||||
|
||||
if (!function_exists('recordRealtimeErrorSpan')) {
|
||||
/** Record a Realtime error onto the active span, or a short-lived `realtime.error` span if there is none. */
|
||||
function recordRealtimeErrorSpan(Throwable $error, string $action, array $tags, ?Document $project, ?Document $user, ?Authorization $authorization): void
|
||||
{
|
||||
$span = Span::current();
|
||||
$ownsSpan = $span === null;
|
||||
if ($ownsSpan) {
|
||||
$span = Span::init('realtime.error');
|
||||
}
|
||||
|
||||
$span->set('realtime.action', $action);
|
||||
$span->set('error.code', $error->getCode());
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$span->set('realtime.projectId', $project->getId());
|
||||
}
|
||||
if ($user !== null && !$user->isEmpty()) {
|
||||
$span->set('realtime.userId', $user->getId());
|
||||
}
|
||||
if ($authorization !== null) {
|
||||
$span->set('realtime.roles', \implode(',', $authorization->getRoles()));
|
||||
}
|
||||
foreach ($tags as $key => $value) {
|
||||
if (\is_scalar($value) || $value === null) {
|
||||
$span->set('realtime.' . $key, $value);
|
||||
}
|
||||
}
|
||||
$span->setError($error);
|
||||
|
||||
if ($ownsSpan) {
|
||||
$span->finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allows overriding
|
||||
if (!function_exists('logError')) {
|
||||
function logError(Throwable $error, string $action, array $tags = [], ?Document $project = null, ?Document $user = null, ?Authorization $authorization = null): void
|
||||
{
|
||||
if (!$error instanceof Exception) {
|
||||
recordRealtimeErrorSpan($error, $action, $tags, $project, $user, $authorization);
|
||||
$span = Span::current();
|
||||
if ($span !== null) {
|
||||
// Per-action error: attach to the active realtime.open / realtime.message / realtime.close
|
||||
// span. The Sentry span exporter in app/init/realtime/span.php ships it with the
|
||||
// operation's full context (attributes, duration, trace_id).
|
||||
$span->set('realtime.action', $action);
|
||||
$span->set('error.code', $error->getCode());
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$span->set('realtime.projectId', $project->getId());
|
||||
}
|
||||
if ($user !== null && !$user->isEmpty()) {
|
||||
$span->set('realtime.userId', $user->getId());
|
||||
}
|
||||
if ($authorization !== null) {
|
||||
$span->set('realtime.roles', \implode(',', $authorization->getRoles()));
|
||||
}
|
||||
foreach ($tags as $key => $value) {
|
||||
if (\is_scalar($value) || $value === null) {
|
||||
$span->set('realtime.' . $key, $value);
|
||||
}
|
||||
}
|
||||
$span->setError($error);
|
||||
} else {
|
||||
// Ad-hoc error (pub/sub subscriber, onStart, the Swoole server error handler,
|
||||
// updateWorkerDocument): not tied to a per-action span. Push to the realtimeLogger
|
||||
// (Sentry / logOwl / Raygun / AppSignal — same dedicated Realtime project).
|
||||
global $register;
|
||||
$logger = $register->get('realtimeLogger');
|
||||
if ($logger) {
|
||||
$log = new Log();
|
||||
$log->setNamespace('realtime');
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion(System::getEnv('_APP_VERSION', 'UNKNOWN'));
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
$log->setAction($action);
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('projectId', $project?->getId() ?: 'n/a');
|
||||
$log->addTag('userId', $user?->getId() ?: 'n/a');
|
||||
foreach ($tags as $key => $value) {
|
||||
$log->addTag($key, $value ?: 'n/a');
|
||||
}
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', $authorization?->getRoles() ?? []);
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
try {
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Error log pushed with status code: ' . $responseCode);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Error pushing log: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
|
||||
Reference in New Issue
Block a user