@k2b/cloud 0.6.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/package.json +128 -0
- package/public/logo.svg +1 -0
- package/scripts/app-favicon.test.ts +49 -0
- package/scripts/app-favicon.ts +51 -0
- package/scripts/build.ts +205 -0
- package/scripts/preload.ts +164 -0
- package/scripts/sync-recovery-smoke.ts +200 -0
- package/src/_internal/app-favicon.ts +2 -0
- package/src/_internal/app-presentation.ts +61 -0
- package/src/_internal/app-workspace-state.ts +24 -0
- package/src/_internal/bounded-json.ts +47 -0
- package/src/_internal/build-metadata.ts +9 -0
- package/src/_internal/capabilities.ts +1118 -0
- package/src/_internal/define-app.ts +801 -0
- package/src/_internal/heartbeat.ts +167 -0
- package/src/_internal/help-catalog.ts +241 -0
- package/src/_internal/help.ts +95 -0
- package/src/_internal/nats-connection.ts +34 -0
- package/src/_internal/page-responses.ts +40 -0
- package/src/_internal/process-sync.ts +117 -0
- package/src/_internal/readiness.ts +3 -0
- package/src/_internal/registry-validation.ts +136 -0
- package/src/_internal/registry-watch.ts +52 -0
- package/src/_internal/registry.ts +338 -0
- package/src/_internal/runtime-compatibility.ts +64 -0
- package/src/_internal/runtime-context.ts +84 -0
- package/src/_internal/runtime-watcher.ts +78 -0
- package/src/_internal/static-assets.ts +86 -0
- package/src/_internal/status-preserving-ssr.ts +49 -0
- package/src/access/PermissionEditor.tsx +486 -0
- package/src/access/ResourceApiKeys.tsx +271 -0
- package/src/access/messages.ts +121 -0
- package/src/access/ui.ts +16 -0
- package/src/account/Avatar.tsx +28 -0
- package/src/account/EntitySearch.tsx +363 -0
- package/src/account/avatar-upload-dialog.tsx +197 -0
- package/src/account/avatar-upload.ts +149 -0
- package/src/account/ui.ts +3 -0
- package/src/ai/admin.ts +47 -0
- package/src/ai/approval-routes.ts +130 -0
- package/src/ai/approvals.ts +139 -0
- package/src/ai/assistant-models.ts +61 -0
- package/src/ai/attachments.ts +40 -0
- package/src/ai/browser.ts +92 -0
- package/src/ai/capabilities.ts +714 -0
- package/src/ai/capability-execution.ts +151 -0
- package/src/ai/chat/blocks.render.test.tsx +1108 -0
- package/src/ai/chat/blocks.tsx +730 -0
- package/src/ai/chat/builtin-tools.tsx +428 -0
- package/src/ai/chat/capability-table.tsx +87 -0
- package/src/ai/chat/composer-adapter.ts +169 -0
- package/src/ai/chat/file-tools.tsx +81 -0
- package/src/ai/chat/message-actions.tsx +370 -0
- package/src/ai/chat/message-utils.ts +479 -0
- package/src/ai/chat/messages.ts +43 -0
- package/src/ai/chat/presentation.tsx +395 -0
- package/src/ai/chat/primitives.tsx +3 -0
- package/src/ai/chat/tool-disclosure.tsx +41 -0
- package/src/ai/chat/turn-navigator-utils.ts +28 -0
- package/src/ai/chat/turn-navigator.tsx +164 -0
- package/src/ai/chat/user-message.tsx +149 -0
- package/src/ai/chat/visual-tools.tsx +435 -0
- package/src/ai/chat/web-tools.tsx +202 -0
- package/src/ai/chat-task-contracts.ts +113 -0
- package/src/ai/chat-tasks.ts +952 -0
- package/src/ai/client/controller.ts +1211 -0
- package/src/ai/client/file-source.ts +95 -0
- package/src/ai/client/live-connection.ts +140 -0
- package/src/ai/client/projection.ts +178 -0
- package/src/ai/client/transport.ts +111 -0
- package/src/ai/compaction.ts +182 -0
- package/src/ai/credentials.ts +124 -0
- package/src/ai/default-tool-contracts.ts +108 -0
- package/src/ai/default-tools.ts +127 -0
- package/src/ai/enrich.ts +280 -0
- package/src/ai/executor.ts +1624 -0
- package/src/ai/fetch-file-tool.ts +313 -0
- package/src/ai/file-context.ts +92 -0
- package/src/ai/file-media-type.ts +28 -0
- package/src/ai/file-mount.ts +16 -0
- package/src/ai/file-tools.ts +566 -0
- package/src/ai/files-store.ts +390 -0
- package/src/ai/firecrawl-tools.ts +305 -0
- package/src/ai/grids-skill.ts +126 -0
- package/src/ai/http.ts +265 -0
- package/src/ai/index.ts +337 -0
- package/src/ai/kit-skill.ts +22 -0
- package/src/ai/limits.ts +5 -0
- package/src/ai/live-events.ts +289 -0
- package/src/ai/live-messages.ts +45 -0
- package/src/ai/live-outbox.ts +102 -0
- package/src/ai/live-routes.ts +412 -0
- package/src/ai/live.ts +2 -0
- package/src/ai/maintenance.ts +302 -0
- package/src/ai/markdown-pdf-tool.ts +95 -0
- package/src/ai/memories.ts +605 -0
- package/src/ai/memory-learning-runs.ts +222 -0
- package/src/ai/memory-learning.ts +619 -0
- package/src/ai/memory-tool.ts +120 -0
- package/src/ai/memory-workflow-evidence.ts +158 -0
- package/src/ai/migrate.ts +2082 -0
- package/src/ai/model-access-migrate.ts +30 -0
- package/src/ai/model-access.ts +191 -0
- package/src/ai/personal-agent.ts +6 -0
- package/src/ai/prefs.ts +72 -0
- package/src/ai/project-tool.ts +94 -0
- package/src/ai/projects-routes.ts +277 -0
- package/src/ai/projects.ts +902 -0
- package/src/ai/protocol.ts +279 -0
- package/src/ai/provider.ts +73 -0
- package/src/ai/public-projection.ts +25 -0
- package/src/ai/resource-markers.ts +31 -0
- package/src/ai/resource-refs.ts +70 -0
- package/src/ai/routes.ts +1131 -0
- package/src/ai/runtime-tools.ts +39 -0
- package/src/ai/runtime.ts +544 -0
- package/src/ai/settings.ts +529 -0
- package/src/ai/short-id.ts +45 -0
- package/src/ai/skill-catalog.ts +73 -0
- package/src/ai/skill-format.ts +145 -0
- package/src/ai/skill-seeds.ts +430 -0
- package/src/ai/skill-tool.ts +81 -0
- package/src/ai/skills-routes.ts +143 -0
- package/src/ai/skills.ts +934 -0
- package/src/ai/solid.ts +27 -0
- package/src/ai/store.ts +3113 -0
- package/src/ai/stream.ts +308 -0
- package/src/ai/structured-runs.ts +66 -0
- package/src/ai/structured.ts +186 -0
- package/src/ai/system-prompt.ts +170 -0
- package/src/ai/task-prompt.ts +22 -0
- package/src/ai/timeline.ts +148 -0
- package/src/ai/tool-audit.ts +177 -0
- package/src/ai/tool-result-budget.ts +16 -0
- package/src/ai/tools.ts +155 -0
- package/src/ai/types.ts +967 -0
- package/src/ai/ui.tsx +39 -0
- package/src/ai/usage-migrate.ts +81 -0
- package/src/ai/usage.ts +315 -0
- package/src/ai/validate.ts +54 -0
- package/src/ai/vision-tool.ts +79 -0
- package/src/api/accounts-entities.ts +174 -0
- package/src/api/admin-ai-projects.ts +74 -0
- package/src/api/admin-ai-skills.ts +76 -0
- package/src/api/admin-ai-usage.ts +42 -0
- package/src/api/admin-core-settings.ts +508 -0
- package/src/api/admin-identity.ts +148 -0
- package/src/api/admin-lifecycle.ts +206 -0
- package/src/api/admin-linux-identities.ts +66 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/app-approval.ts +129 -0
- package/src/api/apps.ts +51 -0
- package/src/api/auth/schemas.ts +54 -0
- package/src/api/auth.ts +370 -0
- package/src/api/capabilities.ts +658 -0
- package/src/api/help-messages.ts +19 -0
- package/src/api/help.ts +78 -0
- package/src/api/index.ts +108 -0
- package/src/api/mcp.ts +744 -0
- package/src/api/me-rail.ts +45 -0
- package/src/api/me.ts +753 -0
- package/src/api/search/schemas.ts +79 -0
- package/src/api/search.bench.ts +185 -0
- package/src/api/search.ts +407 -0
- package/src/api/sync-ops.ts +79 -0
- package/src/api/widgets.ts +143 -0
- package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
- package/src/browser/CloudResourceSearch.tsx +791 -0
- package/src/browser/app-approval-vault.ts +305 -0
- package/src/browser/app-approval.ts +234 -0
- package/src/browser/live-websocket.ts +268 -0
- package/src/browser/locale-preference.ts +25 -0
- package/src/browser/notification-target.ts +27 -0
- package/src/browser/notification-timeout.ts +14 -0
- package/src/browser/notifications.ts +181 -0
- package/src/browser/resource-clipboard.ts +70 -0
- package/src/browser/resource-picker.tsx +39 -0
- package/src/browser/resource-search-messages.ts +55 -0
- package/src/browser/resource-search.ts +27 -0
- package/src/capabilities/client.ts +149 -0
- package/src/capabilities/response.ts +39 -0
- package/src/capabilities/server.ts +215 -0
- package/src/capabilities/signals.ts +4 -0
- package/src/capabilities/testing.ts +12 -0
- package/src/capabilities/types.ts +32 -0
- package/src/cli/access.ts +499 -0
- package/src/cli/account.ts +382 -0
- package/src/cli/admin/account-administration.ts +45 -0
- package/src/cli/admin/account-categories.ts +49 -0
- package/src/cli/admin/ai-usage.ts +118 -0
- package/src/cli/admin/app-sign-in.ts +49 -0
- package/src/cli/admin/data.ts +343 -0
- package/src/cli/admin/documentation.ts +34 -0
- package/src/cli/admin/gateway.ts +155 -0
- package/src/cli/admin/index.ts +61 -0
- package/src/cli/admin/instance.ts +250 -0
- package/src/cli/admin/jobs.ts +223 -0
- package/src/cli/admin/legal.ts +122 -0
- package/src/cli/admin/linux.ts +53 -0
- package/src/cli/admin/logs.ts +324 -0
- package/src/cli/admin/metrics.ts +142 -0
- package/src/cli/admin/nats.ts +167 -0
- package/src/cli/admin/notifications.ts +516 -0
- package/src/cli/admin/shared.ts +113 -0
- package/src/cli/admin/sync.ts +188 -0
- package/src/cli/admin/telemetry.ts +314 -0
- package/src/cli/admin/webhooks.ts +222 -0
- package/src/cli/admin/workflows.ts +367 -0
- package/src/cli/apps.ts +40 -0
- package/src/cli/capabilities.ts +239 -0
- package/src/cli/commands.ts +604 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/locale.ts +31 -0
- package/src/cli/output.ts +58 -0
- package/src/clients/core.ts +20 -0
- package/src/config/env.ts +54 -0
- package/src/config/index.ts +6 -0
- package/src/contracts/account-categories.ts +62 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app-approval.ts +197 -0
- package/src/contracts/app.ts +162 -0
- package/src/contracts/capabilities.ts +484 -0
- package/src/contracts/index.ts +14 -0
- package/src/contracts/notification-live.ts +66 -0
- package/src/contracts/notification-types.ts +235 -0
- package/src/contracts/posix.ts +69 -0
- package/src/contracts/profile.ts +85 -0
- package/src/contracts/rail-preferences.ts +53 -0
- package/src/contracts/registry.ts +110 -0
- package/src/contracts/resource-clipboard.ts +33 -0
- package/src/contracts/settings-types.ts +98 -0
- package/src/contracts/shared.ts +408 -0
- package/src/contracts/user-notifications.ts +101 -0
- package/src/contracts/widgets.ts +183 -0
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/index.ts +58 -0
- package/src/server/actor.ts +41 -0
- package/src/server/api/index.ts +2 -0
- package/src/server/api/respond.ts +96 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/help.ts +187 -0
- package/src/server/index.ts +89 -0
- package/src/server/locale.ts +55 -0
- package/src/server/middleware/auth.ts +386 -0
- package/src/server/middleware/index.ts +28 -0
- package/src/server/middleware/invocation.ts +55 -0
- package/src/server/middleware/middleware.ts +65 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +125 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/route-template.ts +46 -0
- package/src/server/middleware/runtime.ts +22 -0
- package/src/server/middleware/settings.ts +55 -0
- package/src/server/middleware/validator.ts +49 -0
- package/src/server/middleware/workload.ts +22 -0
- package/src/server/ratelimit.ts +151 -0
- package/src/server/request-cookies.ts +18 -0
- package/src/server/services/access.ts +644 -0
- package/src/server/services/freeipa/client.ts +137 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +123 -0
- package/src/server/services/freeipa/test-certificates.ts +44 -0
- package/src/server/services/freeipa/tls.ts +44 -0
- package/src/server/services/freeipa/transport.ts +116 -0
- package/src/server/services/freeipa/util.ts +59 -0
- package/src/server/services/geo.ts +198 -0
- package/src/server/services/index.ts +48 -0
- package/src/server/services/pagination.ts +22 -0
- package/src/server/services/services.ts +13 -0
- package/src/server/time.ts +33 -0
- package/src/services/account-category-policy.ts +51 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +940 -0
- package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
- package/src/services/account-lifecycle/notification-sender.ts +13 -0
- package/src/services/account-lifecycle/scheduler.ts +506 -0
- package/src/services/account-model.ts +22 -0
- package/src/services/accounts/app.ts +1671 -0
- package/src/services/accounts/authz.ts +44 -0
- package/src/services/accounts/avatar.ts +120 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +46 -0
- package/src/services/accounts/duplicate-emails.ts +82 -0
- package/src/services/accounts/entities.ts +622 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +272 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +66 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +499 -0
- package/src/services/accounts/model.ts +129 -0
- package/src/services/accounts/notification-sender.ts +31 -0
- package/src/services/accounts/posix.ts +386 -0
- package/src/services/accounts/request-policy.ts +11 -0
- package/src/services/accounts/switching.ts +126 -0
- package/src/services/accounts/users.ts +745 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/app-approval.ts +498 -0
- package/src/services/audit/index.ts +438 -0
- package/src/services/auth-flows/index.ts +15 -0
- package/src/services/auth-flows/ipa.ts +197 -0
- package/src/services/auth-flows/magic-link.ts +185 -0
- package/src/services/auth-flows/notification-sender.ts +10 -0
- package/src/services/auth-flows/password-reset.ts +268 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/document-extraction.ts +148 -0
- package/src/services/freeipa-config.ts +179 -0
- package/src/services/gateway.ts +186 -0
- package/src/services/identity/constants.ts +18 -0
- package/src/services/identity/index.ts +52 -0
- package/src/services/identity/invocation-actor.ts +120 -0
- package/src/services/identity/invocation-authority.ts +53 -0
- package/src/services/identity/invocation-operations.ts +15 -0
- package/src/services/identity/invocation-token.ts +276 -0
- package/src/services/identity/key-config.ts +41 -0
- package/src/services/identity/key-ring.ts +530 -0
- package/src/services/identity/metrics.ts +29 -0
- package/src/services/identity/routes.ts +18 -0
- package/src/services/identity/runtime-config.ts +88 -0
- package/src/services/identity/session-token.ts +152 -0
- package/src/services/identity/signer-refresh.ts +44 -0
- package/src/services/identity/workload-auth.ts +64 -0
- package/src/services/index.ts +187 -0
- package/src/services/ipa/auth.ts +216 -0
- package/src/services/ipa/connection.ts +57 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/groups.ts +722 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/posix.ts +19 -0
- package/src/services/ipa/profile.ts +132 -0
- package/src/services/ipa/runtime.ts +28 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.ts +100 -0
- package/src/services/ipa/sync.ts +862 -0
- package/src/services/ipa/users.ts +806 -0
- package/src/services/legal-consent.ts +76 -0
- package/src/services/logging/index.ts +477 -0
- package/src/services/logging/redaction.ts +20 -0
- package/src/services/logging/trace.ts +1231 -0
- package/src/services/mandates/index.ts +855 -0
- package/src/services/mandates/policy.ts +85 -0
- package/src/services/network-security.ts +31 -0
- package/src/services/notifications/batches.ts +722 -0
- package/src/services/notifications/browser.ts +252 -0
- package/src/services/notifications/catalog.ts +113 -0
- package/src/services/notifications/channels.ts +104 -0
- package/src/services/notifications/dispatcher.ts +139 -0
- package/src/services/notifications/email.ts +118 -0
- package/src/services/notifications/index.ts +633 -0
- package/src/services/notifications/live.ts +50 -0
- package/src/services/notifications/observability.ts +407 -0
- package/src/services/notifications/platform.ts +421 -0
- package/src/services/notifications/runtime.ts +120 -0
- package/src/services/notifications/user.ts +248 -0
- package/src/services/notifications/web-push-transport.ts +135 -0
- package/src/services/oauth-tokens.ts +209 -0
- package/src/services/outbox.ts +187 -0
- package/src/services/pdf/gotenberg.ts +290 -0
- package/src/services/pdf/index.ts +43 -0
- package/src/services/pdf/markdown.ts +209 -0
- package/src/services/pdf/template-preview.ts +122 -0
- package/src/services/postgres.ts +76 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +51 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +263 -0
- package/src/services/rail-preferences.ts +27 -0
- package/src/services/runtime-lifecycle.ts +191 -0
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.ts +891 -0
- package/src/services/service-accounts.ts +184 -0
- package/src/services/session/index.ts +236 -0
- package/src/services/session/test-fixture.ts +18 -0
- package/src/services/session/user.ts +144 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +86 -0
- package/src/services/settings/core-settings.ts +796 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +374 -0
- package/src/services/settings/index.ts +215 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +241 -0
- package/src/services/settings/templates.ts +4 -0
- package/src/services/sync-ops.ts +403 -0
- package/src/services/topic-cursor.ts +22 -0
- package/src/services/weather/forecast.ts +323 -0
- package/src/services/weather/geo.ts +119 -0
- package/src/services/weather/index.ts +67 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +133 -0
- package/src/services/weather/migrate.ts +45 -0
- package/src/services/weather/settings.ts +66 -0
- package/src/services/weather/short-id.ts +57 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/services/webauthn.ts +385 -0
- package/src/shared/account-action-notice.ts +70 -0
- package/src/shared/account-display.ts +15 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/ai-platform-prompt.ts +134 -0
- package/src/shared/ai-usage.ts +49 -0
- package/src/shared/app-presentation.ts +48 -0
- package/src/shared/app-url.ts +12 -0
- package/src/shared/branding.ts +14 -0
- package/src/shared/capability-messages.ts +70 -0
- package/src/shared/email-html.ts +54 -0
- package/src/shared/format.ts +91 -0
- package/src/shared/help.ts +68 -0
- package/src/shared/icons.ts +400 -0
- package/src/shared/index.ts +54 -0
- package/src/shared/locale.ts +48 -0
- package/src/shared/login-method.ts +44 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +105 -0
- package/src/shared/markdown/extensions/guided-help.ts +100 -0
- package/src/shared/markdown/extensions/images.ts +79 -0
- package/src/shared/markdown/extensions/info-blocks.ts +97 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +43 -0
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +109 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +193 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/network-address.ts +106 -0
- package/src/shared/redirect.ts +56 -0
- package/src/shared/template-rendering.ts +140 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +53 -0
- package/src/ssr/AdminSidebar.tsx +50 -0
- package/src/ssr/AppLaunchpad.island.tsx +236 -0
- package/src/ssr/AppWorkspaceController.island.tsx +21 -0
- package/src/ssr/BrowserNotifications.island.tsx +135 -0
- package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
- package/src/ssr/GlobalSearchDialog.tsx +35 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
- package/src/ssr/Layout.render.test.tsx +148 -0
- package/src/ssr/Layout.tsx +292 -0
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutFooter.tsx +22 -0
- package/src/ssr/LayoutHeader.tsx +81 -0
- package/src/ssr/LayoutHelp.tsx +869 -0
- package/src/ssr/LayoutPreferences.island.tsx +36 -0
- package/src/ssr/LayoutRail.tsx +58 -0
- package/src/ssr/MinimalLayout.tsx +46 -0
- package/src/ssr/PageError.tsx +29 -0
- package/src/ssr/ProfilePreferences.island.tsx +92 -0
- package/src/ssr/RailApps.island.tsx +61 -0
- package/src/ssr/RailEditor.tsx +293 -0
- package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/admin-active-link.ts +47 -0
- package/src/ssr/admin-navigation.ts +78 -0
- package/src/ssr/app-appearance.ts +23 -0
- package/src/ssr/app-navigation.ts +41 -0
- package/src/ssr/help-messages.ts +104 -0
- package/src/ssr/index.ts +14 -0
- package/src/ssr/islands/SearchBar.island.tsx +80 -0
- package/src/ssr/islands/index.ts +16 -0
- package/src/ssr/layout-context.ts +17 -0
- package/src/ssr/layout-help-markdown.ts +9 -0
- package/src/ssr/layout-help-navigation.ts +24 -0
- package/src/ssr/layout-help-url.ts +5 -0
- package/src/ssr/layout-runtime.ts +22 -0
- package/src/ssr/page-error-messages.ts +25 -0
- package/src/ssr/platform-messages.ts +135 -0
- package/src/ssr/preference-controller.ts +44 -0
- package/src/ssr/profile-preferences-messages.ts +29 -0
- package/src/ssr/rail-context.ts +21 -0
- package/src/ssr/rail-messages.ts +66 -0
- package/src/ssr/rail-navigation.ts +60 -0
- package/src/ssr/runtime.ts +31 -0
- package/src/ssr/url-filter.ts +127 -0
- package/src/styles/base-popover.css +36 -0
- package/src/styles/effects.css +851 -0
- package/src/styles/global.css +268 -0
- package/src/styles/input.css +58 -0
- package/src/styles/tokens.css +230 -0
- package/src/styles/utilities-buttons.css +405 -0
- package/src/styles/utilities-data.css +33 -0
- package/src/styles/utilities-feedback.css +232 -0
- package/src/styles/utilities-layout.css +603 -0
- package/src/styles/utilities-markdown-editor.css +525 -0
- package/src/styles/utilities-markdown-table.css +234 -0
- package/src/styles/utilities-navigation.css +1054 -0
- package/src/types/ambient.d.ts +24 -0
- package/src/workflows/ai/index.ts +17 -0
- package/src/workflows/ai/runtime.ts +286 -0
- package/src/workflows/ai/store.ts +314 -0
- package/src/workflows/ai/types.ts +111 -0
- package/src/workflows/ai-actions.ts +269 -0
- package/src/workflows/authoring.ts +88 -0
- package/src/workflows/builtins.ts +193 -0
- package/src/workflows/contracts.ts +280 -0
- package/src/workflows/definition.ts +334 -0
- package/src/workflows/editor.ts +113 -0
- package/src/workflows/index.ts +71 -0
- package/src/workflows/language/binder.ts +45 -0
- package/src/workflows/language/canonical.ts +29 -0
- package/src/workflows/language/compiler.ts +396 -0
- package/src/workflows/language/expressions.ts +34 -0
- package/src/workflows/language/index.ts +20 -0
- package/src/workflows/language/references.ts +55 -0
- package/src/workflows/language/schema.ts +134 -0
- package/src/workflows/language/strict-yaml.ts +131 -0
- package/src/workflows/manifest.ts +24 -0
- package/src/workflows/module.ts +12 -0
- package/src/workflows/runtime/coordinator.ts +115 -0
- package/src/workflows/runtime/executor.ts +1003 -0
- package/src/workflows/runtime/index.ts +60 -0
- package/src/workflows/runtime/instant.ts +8 -0
- package/src/workflows/runtime/ports.ts +213 -0
- package/src/workflows/runtime/schedule.ts +203 -0
- package/src/workflows/runtime/trigger.ts +37 -0
- package/src/workflows/runtime/wake.ts +13 -0
- package/src/workflows/store/actions.ts +438 -0
- package/src/workflows/store/budget.ts +145 -0
- package/src/workflows/store/definitions.ts +345 -0
- package/src/workflows/store/events.ts +383 -0
- package/src/workflows/store/index.ts +107 -0
- package/src/workflows/store/observability.ts +594 -0
- package/src/workflows/store/runs.ts +975 -0
- package/src/workflows/store/transaction.ts +23 -0
- package/src/workflows/store/worker.ts +439 -0
- package/src/workflows/testing/conformance.ts +72 -0
- package/src/workflows/testing/index.ts +11 -0
- package/src/workflows/testing/process-fixtures.ts +275 -0
|
@@ -0,0 +1,1231 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type { SyncEvent } from "@k2b/sync";
|
|
3
|
+
import { sql } from "bun";
|
|
4
|
+
import type { PaginationParams } from "../../contracts/shared";
|
|
5
|
+
import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";
|
|
6
|
+
import { isSensitiveMetadataKey, REDACTED, redactMetadata } from "./redaction";
|
|
7
|
+
|
|
8
|
+
export type TraceAttributeValue = string | number | boolean | null | undefined;
|
|
9
|
+
export type TraceAttributes = Record<string, TraceAttributeValue>;
|
|
10
|
+
export type TraceCategory = "job" | "schedule" | "backfill" | "ai" | "http" | "notification" | "sync" | "custom";
|
|
11
|
+
export type TraceSeverity = "debug" | "info" | "warn" | "error";
|
|
12
|
+
export type TraceSpanKind = "internal" | "server" | "client" | "producer" | "consumer";
|
|
13
|
+
export type TraceStatus = "unset" | "ok" | "error";
|
|
14
|
+
|
|
15
|
+
export type TraceContext = {
|
|
16
|
+
traceId: string;
|
|
17
|
+
spanId: string;
|
|
18
|
+
traceparent: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type TraceSpan = TraceContext & {
|
|
22
|
+
spanKey: string | null;
|
|
23
|
+
parentSpanId: string | null;
|
|
24
|
+
name: string;
|
|
25
|
+
source: string;
|
|
26
|
+
appId: string | null;
|
|
27
|
+
category: TraceCategory;
|
|
28
|
+
kind: TraceSpanKind;
|
|
29
|
+
status: TraceStatus;
|
|
30
|
+
statusMessage: string | null;
|
|
31
|
+
attributes: Record<string, unknown> | null;
|
|
32
|
+
summary: Record<string, unknown> | null;
|
|
33
|
+
eventCount: number;
|
|
34
|
+
startedAt: string;
|
|
35
|
+
endedAt: string | null;
|
|
36
|
+
durationMs: number | null;
|
|
37
|
+
updatedAt: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type TraceEvent = {
|
|
41
|
+
id: string;
|
|
42
|
+
traceId: string;
|
|
43
|
+
spanId: string;
|
|
44
|
+
name: string;
|
|
45
|
+
severity: TraceSeverity;
|
|
46
|
+
attributes: Record<string, unknown> | null;
|
|
47
|
+
body: string | null;
|
|
48
|
+
occurredAt: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type TraceSummary = {
|
|
52
|
+
total: number;
|
|
53
|
+
totalWindow: number;
|
|
54
|
+
running: number;
|
|
55
|
+
succeededWindow: number;
|
|
56
|
+
failedWindow: number;
|
|
57
|
+
sources: number;
|
|
58
|
+
avgDurationMs: number | null;
|
|
59
|
+
p95DurationMs: number | null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type TraceWindow = "10m" | "1h" | "12h" | "24h" | "7d" | "30d";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A span open longer than this is treated as abandoned rather than running,
|
|
66
|
+
* and a span that *closed* after longer than this is treated as anomalous
|
|
67
|
+
* rather than slow.
|
|
68
|
+
*
|
|
69
|
+
* Both cases are real: a process that dies mid-run leaves an open span
|
|
70
|
+
* forever, and a sweep that closes such spans later produces "runs" of
|
|
71
|
+
* several days. Counting either as normal makes "running" report activity
|
|
72
|
+
* that is not happening and drags duration percentiles far off the real
|
|
73
|
+
* distribution, so both are separated out instead of silently averaged in.
|
|
74
|
+
*/
|
|
75
|
+
export const TRACE_STUCK_AFTER_MS = 60 * 60 * 1000;
|
|
76
|
+
|
|
77
|
+
export type TraceRunStats = {
|
|
78
|
+
runs: number;
|
|
79
|
+
sources: number;
|
|
80
|
+
/** Open and started recently — genuinely in flight. */
|
|
81
|
+
running: number;
|
|
82
|
+
/** Open past the abandonment threshold — nothing is working on these. */
|
|
83
|
+
stuck: number;
|
|
84
|
+
/** Closed, but with a duration past the threshold; excluded from percentiles. */
|
|
85
|
+
anomalous: number;
|
|
86
|
+
succeeded: number;
|
|
87
|
+
failed: number;
|
|
88
|
+
errorRate: number;
|
|
89
|
+
avgDurationMs: number | null;
|
|
90
|
+
p95DurationMs: number | null;
|
|
91
|
+
p99DurationMs: number | null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type TraceSourceGroup = {
|
|
95
|
+
source: string;
|
|
96
|
+
appId: string | null;
|
|
97
|
+
categories: TraceCategory[];
|
|
98
|
+
names: string[];
|
|
99
|
+
runs: number;
|
|
100
|
+
jobRuns: number;
|
|
101
|
+
scheduleRuns: number;
|
|
102
|
+
aiRuns: number;
|
|
103
|
+
customRuns: number;
|
|
104
|
+
running: number;
|
|
105
|
+
stuck: number;
|
|
106
|
+
anomalous: number;
|
|
107
|
+
succeeded: number;
|
|
108
|
+
failed: number;
|
|
109
|
+
errorRate: number;
|
|
110
|
+
avgDurationMs: number | null;
|
|
111
|
+
p95DurationMs: number | null;
|
|
112
|
+
p99DurationMs: number | null;
|
|
113
|
+
latestName: string | null;
|
|
114
|
+
latestCategory: TraceCategory | null;
|
|
115
|
+
latestStatus: TraceStatus | null;
|
|
116
|
+
latestStartedAt: string | null;
|
|
117
|
+
latestEndedAt: string | null;
|
|
118
|
+
latestDurationMs: number | null;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type TraceListFilter = {
|
|
122
|
+
appId?: string;
|
|
123
|
+
source?: string;
|
|
124
|
+
sources?: string[];
|
|
125
|
+
status?: TraceStatus;
|
|
126
|
+
active?: boolean;
|
|
127
|
+
category?: TraceCategory;
|
|
128
|
+
attributeEquals?: TraceAttributes;
|
|
129
|
+
search?: string;
|
|
130
|
+
sinceHours?: number;
|
|
131
|
+
sinceSeconds?: number;
|
|
132
|
+
window?: TraceWindow;
|
|
133
|
+
minDurationMs?: number;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type TraceStartParams = {
|
|
137
|
+
name: string;
|
|
138
|
+
source: string;
|
|
139
|
+
spanKey?: string;
|
|
140
|
+
parent?: TraceContext;
|
|
141
|
+
appId?: string;
|
|
142
|
+
category?: TraceCategory;
|
|
143
|
+
kind?: TraceSpanKind;
|
|
144
|
+
attributes?: TraceAttributes | Record<string, unknown>;
|
|
145
|
+
startedAt?: Date | number | string;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export type TraceRecordParams = {
|
|
149
|
+
context?: TraceContext;
|
|
150
|
+
spanKey?: string;
|
|
151
|
+
name?: string;
|
|
152
|
+
source?: string;
|
|
153
|
+
appId?: string;
|
|
154
|
+
category?: TraceCategory;
|
|
155
|
+
kind?: TraceSpanKind;
|
|
156
|
+
event: string;
|
|
157
|
+
severity?: TraceSeverity;
|
|
158
|
+
attributes?: TraceAttributes | Record<string, unknown>;
|
|
159
|
+
body?: string;
|
|
160
|
+
status?: TraceStatus;
|
|
161
|
+
statusMessage?: string;
|
|
162
|
+
summary?: Record<string, unknown>;
|
|
163
|
+
occurredAt?: Date | number | string;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type TraceEndParams = {
|
|
167
|
+
context?: TraceContext;
|
|
168
|
+
spanKey?: string;
|
|
169
|
+
status?: TraceStatus;
|
|
170
|
+
statusMessage?: string;
|
|
171
|
+
summary?: Record<string, unknown>;
|
|
172
|
+
endedAt?: Date | number | string;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export type TraceCompleteParams = TraceStartParams & {
|
|
176
|
+
status?: TraceStatus;
|
|
177
|
+
statusMessage?: string;
|
|
178
|
+
summary?: Record<string, unknown>;
|
|
179
|
+
endedAt?: Date | number | string;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export type TraceWithSpanOptions<T> = {
|
|
183
|
+
summarize?: (result: T) => Record<string, unknown> | undefined;
|
|
184
|
+
onError?: (error: unknown) => Record<string, unknown> | undefined;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
type DbTraceSpanRow = {
|
|
188
|
+
trace_id: string;
|
|
189
|
+
span_id: string;
|
|
190
|
+
span_key: string | null;
|
|
191
|
+
parent_span_id: string | null;
|
|
192
|
+
name: string;
|
|
193
|
+
source: string;
|
|
194
|
+
app_id: string | null;
|
|
195
|
+
category: TraceCategory;
|
|
196
|
+
kind: TraceSpanKind;
|
|
197
|
+
status: TraceStatus;
|
|
198
|
+
status_message: string | null;
|
|
199
|
+
attributes: Record<string, unknown> | string | null;
|
|
200
|
+
summary: Record<string, unknown> | string | null;
|
|
201
|
+
event_count?: number;
|
|
202
|
+
started_at: Date | string;
|
|
203
|
+
ended_at: Date | string | null;
|
|
204
|
+
duration_ms: number | string | null;
|
|
205
|
+
updated_at: Date | string;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
type DbTraceEventRow = {
|
|
209
|
+
id: string | number;
|
|
210
|
+
trace_id: string;
|
|
211
|
+
span_id: string;
|
|
212
|
+
name: string;
|
|
213
|
+
severity: TraceSeverity;
|
|
214
|
+
attributes: Record<string, unknown> | string | null;
|
|
215
|
+
body: string | null;
|
|
216
|
+
occurred_at: Date | string;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
type DbTraceStatsRow = {
|
|
220
|
+
runs: number;
|
|
221
|
+
sources: number;
|
|
222
|
+
running: number;
|
|
223
|
+
stuck: number;
|
|
224
|
+
anomalous: number;
|
|
225
|
+
succeeded: number;
|
|
226
|
+
failed: number;
|
|
227
|
+
error_rate: number | string | null;
|
|
228
|
+
avg_duration_ms: number | string | null;
|
|
229
|
+
p95_duration_ms: number | string | null;
|
|
230
|
+
p99_duration_ms: number | string | null;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
type DbTraceSourceGroupRow = DbTraceStatsRow & {
|
|
234
|
+
source: string;
|
|
235
|
+
app_id: string | null;
|
|
236
|
+
categories: TraceCategory[] | string | null;
|
|
237
|
+
names: string[] | string | null;
|
|
238
|
+
job_runs: number;
|
|
239
|
+
schedule_runs: number;
|
|
240
|
+
ai_runs: number;
|
|
241
|
+
custom_runs: number;
|
|
242
|
+
latest_name: string | null;
|
|
243
|
+
latest_category: TraceCategory | null;
|
|
244
|
+
latest_status: TraceStatus | null;
|
|
245
|
+
latest_started_at: Date | string | null;
|
|
246
|
+
latest_ended_at: Date | string | null;
|
|
247
|
+
latest_duration_ms: number | string | null;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const MAX_TEXT_LENGTH = 2_000;
|
|
251
|
+
const MAX_ARRAY_ITEMS = 50;
|
|
252
|
+
const MAX_OBJECT_KEYS = 100;
|
|
253
|
+
const MAX_JSON_DEPTH = 4;
|
|
254
|
+
const DEFAULT_SOURCE = "trace";
|
|
255
|
+
const TRACE_WINDOW_SECONDS: Record<TraceWindow, number> = {
|
|
256
|
+
"10m": 10 * 60,
|
|
257
|
+
"1h": 60 * 60,
|
|
258
|
+
"12h": 12 * 60 * 60,
|
|
259
|
+
"24h": 24 * 60 * 60,
|
|
260
|
+
"7d": 7 * 24 * 60 * 60,
|
|
261
|
+
"30d": 30 * 24 * 60 * 60,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const normalizeDate = (value: Date | number | string | undefined): Date => {
|
|
265
|
+
if (value instanceof Date) return value;
|
|
266
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
267
|
+
const date = new Date(value);
|
|
268
|
+
if (!Number.isNaN(date.getTime())) return date;
|
|
269
|
+
}
|
|
270
|
+
return new Date();
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const toIso = (value: Date | string): string => (value instanceof Date ? value.toISOString() : new Date(value).toISOString());
|
|
274
|
+
|
|
275
|
+
const toNumberOrNull = (value: number | string | null | undefined): number | null => {
|
|
276
|
+
if (value === null || value === undefined) return null;
|
|
277
|
+
const number = Number(value);
|
|
278
|
+
return Number.isFinite(number) ? number : null;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const toNumber = (value: number | string | null | undefined): number => toNumberOrNull(value) ?? 0;
|
|
282
|
+
|
|
283
|
+
const toStringArray = <T extends string>(value: T[] | string | null | undefined): T[] => {
|
|
284
|
+
if (!value) return [];
|
|
285
|
+
if (Array.isArray(value)) return value.filter(Boolean);
|
|
286
|
+
return value
|
|
287
|
+
.replace(/[{}]/g, "")
|
|
288
|
+
.split(",")
|
|
289
|
+
.map((item) => item.trim())
|
|
290
|
+
.filter(Boolean) as T[];
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const randomHex = (bytes: number): string => randomBytes(bytes).toString("hex");
|
|
294
|
+
|
|
295
|
+
const hashHex = (value: string, bytes: number): string =>
|
|
296
|
+
createHash("sha256")
|
|
297
|
+
.update(value)
|
|
298
|
+
.digest("hex")
|
|
299
|
+
.slice(0, bytes * 2);
|
|
300
|
+
|
|
301
|
+
const contextFor = (traceId: string, spanId: string): TraceContext => ({
|
|
302
|
+
traceId,
|
|
303
|
+
spanId,
|
|
304
|
+
traceparent: `00-${traceId}-${spanId}-01`,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const newContext = (spanKey?: string, parent?: TraceContext): TraceContext => {
|
|
308
|
+
if (spanKey) return contextFor(hashHex(`trace:${spanKey}`, 16), hashHex(`span:${spanKey}`, 8));
|
|
309
|
+
return contextFor(parent?.traceId ?? randomHex(16), randomHex(8));
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const contextFromParams = (params: { context?: TraceContext; spanKey?: string }): TraceContext | null => {
|
|
313
|
+
if (params.context) return params.context;
|
|
314
|
+
if (params.spanKey) return newContext(params.spanKey);
|
|
315
|
+
return null;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const sanitizeJson = (input: unknown, depth = 0): unknown => {
|
|
319
|
+
const redacted = redactMetadata(input);
|
|
320
|
+
if (redacted === null) return null;
|
|
321
|
+
if (typeof redacted === "string") return redacted.length > MAX_TEXT_LENGTH ? `${redacted.slice(0, MAX_TEXT_LENGTH)}...` : redacted;
|
|
322
|
+
if (typeof redacted === "number") return Number.isFinite(redacted) ? redacted : null;
|
|
323
|
+
if (typeof redacted === "boolean") return redacted;
|
|
324
|
+
if (typeof redacted === "bigint") return redacted.toString();
|
|
325
|
+
if (redacted === undefined || typeof redacted === "function" || typeof redacted === "symbol") return null;
|
|
326
|
+
if (depth >= MAX_JSON_DEPTH) return "[Max depth]";
|
|
327
|
+
if (Array.isArray(redacted)) {
|
|
328
|
+
const items = redacted.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeJson(item, depth + 1));
|
|
329
|
+
if (redacted.length > MAX_ARRAY_ITEMS) items.push(`[${redacted.length - MAX_ARRAY_ITEMS} more]`);
|
|
330
|
+
return items;
|
|
331
|
+
}
|
|
332
|
+
if (typeof redacted === "object") {
|
|
333
|
+
const out: Record<string, unknown> = {};
|
|
334
|
+
const entries = Object.entries(redacted as Record<string, unknown>).slice(0, MAX_OBJECT_KEYS);
|
|
335
|
+
for (const [key, value] of entries) {
|
|
336
|
+
out[key] = isSensitiveMetadataKey(key) ? REDACTED : sanitizeJson(value, depth + 1);
|
|
337
|
+
}
|
|
338
|
+
const extra = Object.keys(redacted as Record<string, unknown>).length - entries.length;
|
|
339
|
+
if (extra > 0) out.__truncatedKeys = extra;
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const sanitizeRecord = (input: Record<string, unknown> | TraceAttributes | undefined): Record<string, unknown> | null => {
|
|
346
|
+
if (!input) return null;
|
|
347
|
+
const sanitized = sanitizeJson(input);
|
|
348
|
+
if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) return null;
|
|
349
|
+
return sanitized as Record<string, unknown>;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const sanitizeAttributes = (input: Record<string, unknown> | TraceAttributes | undefined): Record<string, unknown> | null => {
|
|
353
|
+
const record = sanitizeRecord(input);
|
|
354
|
+
if (!record) return null;
|
|
355
|
+
const out: Record<string, unknown> = {};
|
|
356
|
+
for (const [key, value] of Object.entries(record)) {
|
|
357
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
358
|
+
out[key] = value;
|
|
359
|
+
} else {
|
|
360
|
+
out[key] = JSON.stringify(value);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const errorAttributes = (error: Error): TraceAttributes => ({
|
|
367
|
+
"exception.type": error.name || "Error",
|
|
368
|
+
"exception.message": error.message,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const mapSpanRow = (row: DbTraceSpanRow): TraceSpan => {
|
|
372
|
+
const context = contextFor(row.trace_id, row.span_id);
|
|
373
|
+
return {
|
|
374
|
+
...context,
|
|
375
|
+
spanKey: row.span_key,
|
|
376
|
+
parentSpanId: row.parent_span_id,
|
|
377
|
+
name: row.name,
|
|
378
|
+
source: row.source,
|
|
379
|
+
appId: row.app_id,
|
|
380
|
+
category: row.category,
|
|
381
|
+
kind: row.kind,
|
|
382
|
+
status: row.status,
|
|
383
|
+
statusMessage: row.status_message,
|
|
384
|
+
attributes: parsePgJsonRecord(row.attributes),
|
|
385
|
+
summary: parsePgJsonRecord(row.summary),
|
|
386
|
+
eventCount: Number(row.event_count ?? 0),
|
|
387
|
+
startedAt: toIso(row.started_at),
|
|
388
|
+
endedAt: row.ended_at ? toIso(row.ended_at) : null,
|
|
389
|
+
durationMs: toNumberOrNull(row.duration_ms),
|
|
390
|
+
updatedAt: toIso(row.updated_at),
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const mapEventRow = (row: DbTraceEventRow): TraceEvent => ({
|
|
395
|
+
id: String(row.id),
|
|
396
|
+
traceId: row.trace_id,
|
|
397
|
+
spanId: row.span_id,
|
|
398
|
+
name: row.name,
|
|
399
|
+
severity: row.severity,
|
|
400
|
+
attributes: parsePgJsonRecord(row.attributes),
|
|
401
|
+
body: row.body,
|
|
402
|
+
occurredAt: toIso(row.occurred_at),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const mapStatsRow = (row: DbTraceStatsRow | undefined): TraceRunStats => ({
|
|
406
|
+
runs: row?.runs ?? 0,
|
|
407
|
+
sources: row?.sources ?? 0,
|
|
408
|
+
running: row?.running ?? 0,
|
|
409
|
+
stuck: row?.stuck ?? 0,
|
|
410
|
+
anomalous: row?.anomalous ?? 0,
|
|
411
|
+
succeeded: row?.succeeded ?? 0,
|
|
412
|
+
failed: row?.failed ?? 0,
|
|
413
|
+
errorRate: toNumber(row?.error_rate),
|
|
414
|
+
avgDurationMs: toNumberOrNull(row?.avg_duration_ms),
|
|
415
|
+
p95DurationMs: toNumberOrNull(row?.p95_duration_ms),
|
|
416
|
+
p99DurationMs: toNumberOrNull(row?.p99_duration_ms),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const mapSourceGroupRow = (row: DbTraceSourceGroupRow): TraceSourceGroup => ({
|
|
420
|
+
source: row.source,
|
|
421
|
+
appId: row.app_id,
|
|
422
|
+
categories: toStringArray(row.categories),
|
|
423
|
+
names: toStringArray(row.names),
|
|
424
|
+
runs: row.runs,
|
|
425
|
+
jobRuns: row.job_runs,
|
|
426
|
+
scheduleRuns: row.schedule_runs,
|
|
427
|
+
aiRuns: row.ai_runs,
|
|
428
|
+
customRuns: row.custom_runs,
|
|
429
|
+
running: row.running,
|
|
430
|
+
stuck: row.stuck,
|
|
431
|
+
anomalous: row.anomalous,
|
|
432
|
+
succeeded: row.succeeded,
|
|
433
|
+
failed: row.failed,
|
|
434
|
+
errorRate: toNumber(row.error_rate),
|
|
435
|
+
avgDurationMs: toNumberOrNull(row.avg_duration_ms),
|
|
436
|
+
p95DurationMs: toNumberOrNull(row.p95_duration_ms),
|
|
437
|
+
p99DurationMs: toNumberOrNull(row.p99_duration_ms),
|
|
438
|
+
latestName: row.latest_name,
|
|
439
|
+
latestCategory: row.latest_category,
|
|
440
|
+
latestStatus: row.latest_status,
|
|
441
|
+
latestStartedAt: row.latest_started_at ? toIso(row.latest_started_at) : null,
|
|
442
|
+
latestEndedAt: row.latest_ended_at ? toIso(row.latest_ended_at) : null,
|
|
443
|
+
latestDurationMs: toNumberOrNull(row.latest_duration_ms),
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const updateSpan = async (
|
|
447
|
+
context: TraceContext,
|
|
448
|
+
params: {
|
|
449
|
+
status?: TraceStatus;
|
|
450
|
+
statusMessage?: string;
|
|
451
|
+
summary?: Record<string, unknown>;
|
|
452
|
+
endedAt?: Date;
|
|
453
|
+
},
|
|
454
|
+
): Promise<void> => {
|
|
455
|
+
const summary = sanitizeRecord(params.summary);
|
|
456
|
+
const summaryJson = summary ? JSON.stringify(summary) : null;
|
|
457
|
+
const statusMessage = params.statusMessage ? String(sanitizeJson(params.statusMessage)) : null;
|
|
458
|
+
await sql`
|
|
459
|
+
UPDATE logging.trace_spans
|
|
460
|
+
SET
|
|
461
|
+
status = COALESCE(${params.status ?? null}::text, status),
|
|
462
|
+
status_message = COALESCE(${statusMessage}::text, status_message),
|
|
463
|
+
summary = CASE
|
|
464
|
+
WHEN (${summaryJson}::text)::jsonb IS NULL THEN logging.trace_spans.summary
|
|
465
|
+
ELSE COALESCE(logging.trace_spans.summary, '{}'::jsonb) || (${summaryJson}::text)::jsonb
|
|
466
|
+
END,
|
|
467
|
+
ended_at = COALESCE(${params.endedAt ?? null}, ended_at),
|
|
468
|
+
duration_ms = CASE
|
|
469
|
+
WHEN COALESCE(${params.endedAt ?? null}, ended_at) IS NULL THEN duration_ms
|
|
470
|
+
ELSE GREATEST(0, EXTRACT(EPOCH FROM (COALESCE(${params.endedAt ?? null}, ended_at) - started_at)) * 1000)
|
|
471
|
+
END,
|
|
472
|
+
updated_at = now()
|
|
473
|
+
WHERE trace_id = ${context.traceId} AND span_id = ${context.spanId}
|
|
474
|
+
`.catch((error: Error) => console.error("[logging:trace] span update failed:", error.message));
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const start = async (params: TraceStartParams, preservePresentation = false): Promise<TraceContext> => {
|
|
478
|
+
const context = newContext(params.spanKey, params.parent);
|
|
479
|
+
const attributes = sanitizeAttributes(params.attributes);
|
|
480
|
+
const attributesJson = attributes ? JSON.stringify(attributes) : null;
|
|
481
|
+
const startedAt = normalizeDate(params.startedAt);
|
|
482
|
+
const [stored] = await sql<Array<{ trace_id: string; span_id: string }>>`
|
|
483
|
+
INSERT INTO logging.trace_spans (
|
|
484
|
+
trace_id,
|
|
485
|
+
span_id,
|
|
486
|
+
span_key,
|
|
487
|
+
parent_span_id,
|
|
488
|
+
name,
|
|
489
|
+
source,
|
|
490
|
+
app_id,
|
|
491
|
+
category,
|
|
492
|
+
kind,
|
|
493
|
+
status,
|
|
494
|
+
attributes,
|
|
495
|
+
started_at,
|
|
496
|
+
updated_at
|
|
497
|
+
)
|
|
498
|
+
VALUES (
|
|
499
|
+
${context.traceId},
|
|
500
|
+
${context.spanId},
|
|
501
|
+
${params.spanKey ?? null},
|
|
502
|
+
${params.parent?.spanId ?? null},
|
|
503
|
+
${params.name},
|
|
504
|
+
${params.source},
|
|
505
|
+
${params.appId ?? null},
|
|
506
|
+
${params.category ?? "custom"},
|
|
507
|
+
${params.kind ?? "internal"},
|
|
508
|
+
'unset',
|
|
509
|
+
(${attributesJson}::text)::jsonb,
|
|
510
|
+
${startedAt},
|
|
511
|
+
now()
|
|
512
|
+
)
|
|
513
|
+
ON CONFLICT (trace_id, span_id) DO UPDATE
|
|
514
|
+
SET
|
|
515
|
+
span_key = COALESCE(logging.trace_spans.span_key, EXCLUDED.span_key),
|
|
516
|
+
parent_span_id = COALESCE(logging.trace_spans.parent_span_id, EXCLUDED.parent_span_id),
|
|
517
|
+
name = CASE WHEN ${preservePresentation} THEN logging.trace_spans.name ELSE EXCLUDED.name END,
|
|
518
|
+
source = CASE WHEN ${preservePresentation} THEN logging.trace_spans.source ELSE EXCLUDED.source END,
|
|
519
|
+
app_id = COALESCE(EXCLUDED.app_id, logging.trace_spans.app_id),
|
|
520
|
+
category = CASE WHEN ${preservePresentation} THEN logging.trace_spans.category ELSE EXCLUDED.category END,
|
|
521
|
+
kind = CASE WHEN ${preservePresentation} THEN logging.trace_spans.kind ELSE EXCLUDED.kind END,
|
|
522
|
+
attributes = COALESCE(logging.trace_spans.attributes, '{}'::jsonb) || COALESCE(EXCLUDED.attributes, '{}'::jsonb),
|
|
523
|
+
started_at = LEAST(logging.trace_spans.started_at, EXCLUDED.started_at),
|
|
524
|
+
updated_at = now()
|
|
525
|
+
RETURNING trace_id, span_id
|
|
526
|
+
`.catch((error: Error) => {
|
|
527
|
+
console.error("[logging:trace] span start failed:", error.message);
|
|
528
|
+
return [];
|
|
529
|
+
});
|
|
530
|
+
return stored ? contextFor(stored.trace_id, stored.span_id) : context;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Persists a span whose complete lifecycle is already known. Hot request paths
|
|
535
|
+
* can retain exact tracing without a start/update round-trip pair.
|
|
536
|
+
*/
|
|
537
|
+
const complete = async (params: TraceCompleteParams, preservePresentation = false): Promise<TraceContext> => {
|
|
538
|
+
const context = newContext(params.spanKey, params.parent);
|
|
539
|
+
const attributes = sanitizeAttributes(params.attributes);
|
|
540
|
+
const summary = sanitizeRecord(params.summary);
|
|
541
|
+
const attributesJson = attributes ? JSON.stringify(attributes) : null;
|
|
542
|
+
const summaryJson = summary ? JSON.stringify(summary) : null;
|
|
543
|
+
const statusMessage = params.statusMessage ? String(sanitizeJson(params.statusMessage)) : null;
|
|
544
|
+
const startedAt = normalizeDate(params.startedAt);
|
|
545
|
+
const endedAt = normalizeDate(params.endedAt);
|
|
546
|
+
const [stored] = await sql<Array<{ trace_id: string; span_id: string }>>`
|
|
547
|
+
INSERT INTO logging.trace_spans (
|
|
548
|
+
trace_id,
|
|
549
|
+
span_id,
|
|
550
|
+
span_key,
|
|
551
|
+
parent_span_id,
|
|
552
|
+
name,
|
|
553
|
+
source,
|
|
554
|
+
app_id,
|
|
555
|
+
category,
|
|
556
|
+
kind,
|
|
557
|
+
status,
|
|
558
|
+
status_message,
|
|
559
|
+
attributes,
|
|
560
|
+
summary,
|
|
561
|
+
started_at,
|
|
562
|
+
ended_at,
|
|
563
|
+
duration_ms,
|
|
564
|
+
updated_at
|
|
565
|
+
)
|
|
566
|
+
VALUES (
|
|
567
|
+
${context.traceId},
|
|
568
|
+
${context.spanId},
|
|
569
|
+
${params.spanKey ?? null},
|
|
570
|
+
${params.parent?.spanId ?? null},
|
|
571
|
+
${params.name},
|
|
572
|
+
${params.source},
|
|
573
|
+
${params.appId ?? null},
|
|
574
|
+
${params.category ?? "custom"},
|
|
575
|
+
${params.kind ?? "internal"},
|
|
576
|
+
${params.status ?? "ok"},
|
|
577
|
+
${statusMessage},
|
|
578
|
+
(${attributesJson}::text)::jsonb,
|
|
579
|
+
(${summaryJson}::text)::jsonb,
|
|
580
|
+
${startedAt},
|
|
581
|
+
${endedAt},
|
|
582
|
+
GREATEST(0, EXTRACT(EPOCH FROM (${endedAt}::timestamptz - ${startedAt}::timestamptz)) * 1000),
|
|
583
|
+
now()
|
|
584
|
+
)
|
|
585
|
+
ON CONFLICT (trace_id, span_id) DO UPDATE
|
|
586
|
+
SET
|
|
587
|
+
span_key = COALESCE(logging.trace_spans.span_key, EXCLUDED.span_key),
|
|
588
|
+
parent_span_id = COALESCE(logging.trace_spans.parent_span_id, EXCLUDED.parent_span_id),
|
|
589
|
+
name = CASE WHEN ${preservePresentation} THEN logging.trace_spans.name ELSE EXCLUDED.name END,
|
|
590
|
+
source = CASE WHEN ${preservePresentation} THEN logging.trace_spans.source ELSE EXCLUDED.source END,
|
|
591
|
+
app_id = COALESCE(EXCLUDED.app_id, logging.trace_spans.app_id),
|
|
592
|
+
category = CASE WHEN ${preservePresentation} THEN logging.trace_spans.category ELSE EXCLUDED.category END,
|
|
593
|
+
kind = CASE WHEN ${preservePresentation} THEN logging.trace_spans.kind ELSE EXCLUDED.kind END,
|
|
594
|
+
status = EXCLUDED.status,
|
|
595
|
+
status_message = EXCLUDED.status_message,
|
|
596
|
+
attributes = COALESCE(logging.trace_spans.attributes, '{}'::jsonb) || COALESCE(EXCLUDED.attributes, '{}'::jsonb),
|
|
597
|
+
summary = COALESCE(logging.trace_spans.summary, '{}'::jsonb) || COALESCE(EXCLUDED.summary, '{}'::jsonb),
|
|
598
|
+
started_at = LEAST(logging.trace_spans.started_at, EXCLUDED.started_at),
|
|
599
|
+
ended_at = GREATEST(logging.trace_spans.ended_at, EXCLUDED.ended_at),
|
|
600
|
+
duration_ms = GREATEST(
|
|
601
|
+
0,
|
|
602
|
+
EXTRACT(EPOCH FROM (GREATEST(logging.trace_spans.ended_at, EXCLUDED.ended_at) - LEAST(logging.trace_spans.started_at, EXCLUDED.started_at))) * 1000
|
|
603
|
+
),
|
|
604
|
+
updated_at = now()
|
|
605
|
+
RETURNING trace_id, span_id
|
|
606
|
+
`.catch((error: Error) => {
|
|
607
|
+
console.error("[logging:trace] completed span write failed:", error.message);
|
|
608
|
+
return [];
|
|
609
|
+
});
|
|
610
|
+
return stored ? contextFor(stored.trace_id, stored.span_id) : context;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const record = async (params: TraceRecordParams, preservePresentation = false): Promise<TraceContext> => {
|
|
614
|
+
const standalone = !params.context && !params.spanKey;
|
|
615
|
+
const context =
|
|
616
|
+
params.context ??
|
|
617
|
+
(await start(
|
|
618
|
+
{
|
|
619
|
+
name: params.name ?? params.event,
|
|
620
|
+
source: params.source ?? DEFAULT_SOURCE,
|
|
621
|
+
spanKey: params.spanKey,
|
|
622
|
+
appId: params.appId,
|
|
623
|
+
category: params.category ?? "custom",
|
|
624
|
+
kind: params.kind ?? "internal",
|
|
625
|
+
startedAt: params.occurredAt,
|
|
626
|
+
},
|
|
627
|
+
preservePresentation,
|
|
628
|
+
));
|
|
629
|
+
const occurredAt = normalizeDate(params.occurredAt);
|
|
630
|
+
const attributes = sanitizeAttributes(params.attributes);
|
|
631
|
+
const attributesJson = attributes ? JSON.stringify(attributes) : null;
|
|
632
|
+
const body = params.body ? String(sanitizeJson(params.body)) : null;
|
|
633
|
+
await sql`
|
|
634
|
+
INSERT INTO logging.trace_events (trace_id, span_id, name, severity, attributes, body, occurred_at)
|
|
635
|
+
VALUES (
|
|
636
|
+
${context.traceId},
|
|
637
|
+
${context.spanId},
|
|
638
|
+
${params.event},
|
|
639
|
+
${params.severity ?? "info"},
|
|
640
|
+
(${attributesJson}::text)::jsonb,
|
|
641
|
+
${body},
|
|
642
|
+
${occurredAt}
|
|
643
|
+
)
|
|
644
|
+
`.catch((error: Error) => console.error("[logging:trace] event write failed:", error.message));
|
|
645
|
+
if (params.status || params.statusMessage || params.summary) {
|
|
646
|
+
await updateSpan(context, { status: params.status, statusMessage: params.statusMessage, summary: params.summary });
|
|
647
|
+
}
|
|
648
|
+
if (standalone) await end({ context, status: params.status ?? "unset", summary: params.summary, endedAt: occurredAt });
|
|
649
|
+
return context;
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const end = async (params: TraceEndParams): Promise<void> => {
|
|
653
|
+
// An application may attach a summary immediately after Sync emits start.
|
|
654
|
+
// Wait for that queued insert before updating the same deterministic span.
|
|
655
|
+
if (params.spanKey) await pendingSyncWrites.get(params.spanKey);
|
|
656
|
+
const context = contextFromParams(params);
|
|
657
|
+
if (!context) return;
|
|
658
|
+
await updateSpan(context, {
|
|
659
|
+
status: params.status ?? "ok",
|
|
660
|
+
statusMessage: params.statusMessage,
|
|
661
|
+
summary: params.summary,
|
|
662
|
+
endedAt: normalizeDate(params.endedAt),
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const withSpan = async <T>(
|
|
667
|
+
params: TraceStartParams,
|
|
668
|
+
fn: (span: TraceContext) => Promise<T> | T,
|
|
669
|
+
options?: TraceWithSpanOptions<T>,
|
|
670
|
+
): Promise<T> => {
|
|
671
|
+
const span = await start(params);
|
|
672
|
+
try {
|
|
673
|
+
const result = await fn(span);
|
|
674
|
+
await end({ context: span, status: "ok", summary: options?.summarize?.(result) });
|
|
675
|
+
return result;
|
|
676
|
+
} catch (error) {
|
|
677
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
678
|
+
await record({
|
|
679
|
+
context: span,
|
|
680
|
+
event: "exception",
|
|
681
|
+
severity: "error",
|
|
682
|
+
attributes: error instanceof Error ? errorAttributes(error) : { "exception.message": message },
|
|
683
|
+
});
|
|
684
|
+
await end({ context: span, status: "error", statusMessage: message, summary: options?.onError?.(error) });
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const normalizeSinceHours = (sinceHours: number | undefined): number | null =>
|
|
690
|
+
Number.isFinite(sinceHours) && sinceHours && sinceHours > 0 ? Math.trunc(sinceHours) : null;
|
|
691
|
+
|
|
692
|
+
const normalizeSinceSeconds = (filter: TraceListFilter | undefined): number | null => {
|
|
693
|
+
if (Number.isFinite(filter?.sinceSeconds) && filter?.sinceSeconds && filter.sinceSeconds > 0) return Math.trunc(filter.sinceSeconds);
|
|
694
|
+
if (filter?.window) return TRACE_WINDOW_SECONDS[filter.window];
|
|
695
|
+
const sinceHours = normalizeSinceHours(filter?.sinceHours);
|
|
696
|
+
return sinceHours ? sinceHours * 60 * 60 : null;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const normalizeMinDurationMs = (value: number | undefined): number | null =>
|
|
700
|
+
Number.isFinite(value) && value && value > 0 ? Math.trunc(value) : null;
|
|
701
|
+
|
|
702
|
+
const normalizeLimit = (limit: number | undefined): number => Math.min(Math.max(Math.trunc(limit ?? 50), 1), 200);
|
|
703
|
+
|
|
704
|
+
const traceConditions = (filter: TraceListFilter | undefined): any[] => {
|
|
705
|
+
const conditions: any[] = [sql`TRUE`];
|
|
706
|
+
const searchPattern = filter?.search ? `%${escapeLikePattern(filter.search)}%` : null;
|
|
707
|
+
const sources = filter?.sources?.map((value) => value.trim()).filter(Boolean);
|
|
708
|
+
const sourceList = sources && sources.length > 0 ? toPgTextArray([...new Set(sources)]) : null;
|
|
709
|
+
const sinceSeconds = normalizeSinceSeconds(filter);
|
|
710
|
+
const minDurationMs = normalizeMinDurationMs(filter?.minDurationMs);
|
|
711
|
+
const attributeEquals = sanitizeAttributes(filter?.attributeEquals);
|
|
712
|
+
|
|
713
|
+
if (filter?.appId) conditions.push(sql`s.app_id = ${filter.appId}`);
|
|
714
|
+
if (filter?.source) conditions.push(sql`s.source = ${filter.source}`);
|
|
715
|
+
if (sourceList && !filter?.source) conditions.push(sql`s.source = ANY(${sourceList}::text[])`);
|
|
716
|
+
if (filter?.status) conditions.push(sql`s.status = ${filter.status}`);
|
|
717
|
+
if (filter?.active === true) conditions.push(sql`s.ended_at IS NULL`);
|
|
718
|
+
if (filter?.active === false) conditions.push(sql`s.ended_at IS NOT NULL`);
|
|
719
|
+
if (filter?.category) conditions.push(sql`s.category = ${filter.category}`);
|
|
720
|
+
if (attributeEquals && Object.keys(attributeEquals).length > 0) {
|
|
721
|
+
conditions.push(sql`s.attributes @> (${JSON.stringify(attributeEquals)}::text)::jsonb`);
|
|
722
|
+
}
|
|
723
|
+
if (sinceSeconds) conditions.push(sql`s.started_at >= now() - (${sinceSeconds}::int * INTERVAL '1 second')`);
|
|
724
|
+
if (minDurationMs) conditions.push(sql`s.duration_ms IS NOT NULL AND s.duration_ms > ${minDurationMs}`);
|
|
725
|
+
if (searchPattern) {
|
|
726
|
+
conditions.push(sql`(
|
|
727
|
+
s.name ILIKE ${searchPattern} ESCAPE '\'
|
|
728
|
+
OR s.source ILIKE ${searchPattern} ESCAPE '\'
|
|
729
|
+
OR COALESCE(s.app_id, '') ILIKE ${searchPattern} ESCAPE '\'
|
|
730
|
+
OR COALESCE(s.span_key, '') ILIKE ${searchPattern} ESCAPE '\'
|
|
731
|
+
)`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return conditions;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const traceWhere = (filter: TraceListFilter | undefined) =>
|
|
738
|
+
traceConditions(filter).reduce((acc, condition) => sql`${acc} AND ${condition}`);
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Abandoned spans are old by definition, so a windowed count hides exactly the
|
|
742
|
+
* ones worth seeing: with a 24h window, a span orphaned three days ago reports
|
|
743
|
+
* zero. Stuck is therefore counted across all retained spans, with the rest of
|
|
744
|
+
* the filter (source, category, search) still applied.
|
|
745
|
+
*/
|
|
746
|
+
const stuckWhere = (filter: TraceListFilter | undefined) =>
|
|
747
|
+
traceConditions({ ...filter, window: undefined })
|
|
748
|
+
.concat([sql`s.ended_at IS NULL`, sql`s.started_at < now() - (${TRACE_STUCK_AFTER_MS}::bigint * INTERVAL '1 millisecond')`])
|
|
749
|
+
.reduce((acc, condition) => sql`${acc} AND ${condition}`);
|
|
750
|
+
|
|
751
|
+
const countStuck = async (filter: TraceListFilter | undefined): Promise<number> => {
|
|
752
|
+
const [row] = await sql<{ count: number }[]>`
|
|
753
|
+
SELECT COUNT(*)::int AS count FROM logging.trace_spans s WHERE ${stuckWhere(filter)}
|
|
754
|
+
`;
|
|
755
|
+
return row?.count ?? 0;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const countStuckBySource = async (filter: TraceListFilter | undefined): Promise<Map<string, number>> => {
|
|
759
|
+
const rows = await sql<{ source: string; count: number }[]>`
|
|
760
|
+
SELECT s.source, COUNT(*)::int AS count
|
|
761
|
+
FROM logging.trace_spans s WHERE ${stuckWhere(filter)}
|
|
762
|
+
GROUP BY s.source
|
|
763
|
+
`;
|
|
764
|
+
return new Map(rows.map((row) => [row.source, row.count]));
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const list = async (
|
|
768
|
+
pagination: PaginationParams,
|
|
769
|
+
options?: {
|
|
770
|
+
filter?: TraceListFilter;
|
|
771
|
+
},
|
|
772
|
+
): Promise<{ spans: TraceSpan[]; total: number }> => {
|
|
773
|
+
const { offset, perPage } = pagination;
|
|
774
|
+
const where = traceWhere(options?.filter);
|
|
775
|
+
const [countRows, dataRows] = await Promise.all([
|
|
776
|
+
sql<{ count: number }[]>`
|
|
777
|
+
SELECT COUNT(*)::int AS count
|
|
778
|
+
FROM logging.trace_spans s
|
|
779
|
+
WHERE ${where}
|
|
780
|
+
`,
|
|
781
|
+
sql<DbTraceSpanRow[]>`
|
|
782
|
+
SELECT
|
|
783
|
+
s.*,
|
|
784
|
+
(
|
|
785
|
+
SELECT COUNT(*)::int
|
|
786
|
+
FROM logging.trace_events e
|
|
787
|
+
WHERE e.trace_id = s.trace_id AND e.span_id = s.span_id
|
|
788
|
+
) AS event_count
|
|
789
|
+
FROM logging.trace_spans s
|
|
790
|
+
WHERE ${where}
|
|
791
|
+
ORDER BY COALESCE(s.ended_at, s.updated_at, s.started_at) DESC
|
|
792
|
+
LIMIT ${perPage} OFFSET ${offset}
|
|
793
|
+
`,
|
|
794
|
+
]);
|
|
795
|
+
|
|
796
|
+
return { spans: dataRows.map(mapSpanRow), total: countRows[0]?.count ?? 0 };
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const getSpan = async (params: { traceId: string; spanId: string }): Promise<TraceSpan | null> => {
|
|
800
|
+
const rows = await sql<DbTraceSpanRow[]>`
|
|
801
|
+
SELECT
|
|
802
|
+
s.*,
|
|
803
|
+
(
|
|
804
|
+
SELECT COUNT(*)::int
|
|
805
|
+
FROM logging.trace_events e
|
|
806
|
+
WHERE e.trace_id = s.trace_id AND e.span_id = s.span_id
|
|
807
|
+
) AS event_count
|
|
808
|
+
FROM logging.trace_spans s
|
|
809
|
+
WHERE s.trace_id = ${params.traceId} AND s.span_id = ${params.spanId}
|
|
810
|
+
LIMIT 1
|
|
811
|
+
`;
|
|
812
|
+
return rows[0] ? mapSpanRow(rows[0]) : null;
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const stats = async (options?: { filter?: TraceListFilter }): Promise<TraceRunStats> => {
|
|
816
|
+
const where = traceWhere(options?.filter);
|
|
817
|
+
const [row] = await sql<DbTraceStatsRow[]>`
|
|
818
|
+
SELECT
|
|
819
|
+
COUNT(*)::int AS runs,
|
|
820
|
+
COUNT(DISTINCT s.source)::int AS sources,
|
|
821
|
+
COUNT(*) FILTER (WHERE s.ended_at IS NULL AND s.started_at >= now() - (${TRACE_STUCK_AFTER_MS}::bigint * INTERVAL '1 millisecond'))::int AS running,
|
|
822
|
+
COUNT(*) FILTER (WHERE s.ended_at IS NULL AND s.started_at < now() - (${TRACE_STUCK_AFTER_MS}::bigint * INTERVAL '1 millisecond'))::int AS stuck,
|
|
823
|
+
COUNT(*) FILTER (WHERE s.duration_ms > ${TRACE_STUCK_AFTER_MS})::int AS anomalous,
|
|
824
|
+
COUNT(*) FILTER (WHERE s.status = 'ok')::int AS succeeded,
|
|
825
|
+
COUNT(*) FILTER (WHERE s.status = 'error')::int AS failed,
|
|
826
|
+
COALESCE(ROUND((COUNT(*) FILTER (WHERE s.status = 'error'))::numeric * 100 / NULLIF(COUNT(*), 0), 2), 0)::float AS error_rate,
|
|
827
|
+
(AVG(s.duration_ms) FILTER (WHERE s.duration_ms IS NOT NULL AND s.duration_ms <= ${TRACE_STUCK_AFTER_MS}))::float AS avg_duration_ms,
|
|
828
|
+
(percentile_cont(0.95) WITHIN GROUP (ORDER BY s.duration_ms) FILTER (WHERE s.duration_ms IS NOT NULL AND s.duration_ms <= ${TRACE_STUCK_AFTER_MS}))::float AS p95_duration_ms,
|
|
829
|
+
(percentile_cont(0.99) WITHIN GROUP (ORDER BY s.duration_ms) FILTER (WHERE s.duration_ms IS NOT NULL AND s.duration_ms <= ${TRACE_STUCK_AFTER_MS}))::float AS p99_duration_ms
|
|
830
|
+
FROM logging.trace_spans s
|
|
831
|
+
WHERE ${where}
|
|
832
|
+
`;
|
|
833
|
+
return { ...mapStatsRow(row), stuck: await countStuck(options?.filter) };
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
const sourceGroups = async (options?: { filter?: TraceListFilter }): Promise<TraceSourceGroup[]> => {
|
|
837
|
+
const where = traceWhere(options?.filter);
|
|
838
|
+
const stuckBySource = await countStuckBySource(options?.filter);
|
|
839
|
+
const rows = await sql<DbTraceSourceGroupRow[]>`
|
|
840
|
+
WITH grouped AS (
|
|
841
|
+
SELECT
|
|
842
|
+
s.source,
|
|
843
|
+
(array_remove(array_agg(DISTINCT s.app_id), NULL))[1] AS app_id,
|
|
844
|
+
array_agg(DISTINCT s.category ORDER BY s.category)::text[] AS categories,
|
|
845
|
+
array_agg(DISTINCT s.name ORDER BY s.name)::text[] AS names,
|
|
846
|
+
COUNT(*)::int AS runs,
|
|
847
|
+
COUNT(DISTINCT s.source)::int AS sources,
|
|
848
|
+
COUNT(*) FILTER (WHERE s.category = 'job')::int AS job_runs,
|
|
849
|
+
COUNT(*) FILTER (WHERE s.category = 'schedule')::int AS schedule_runs,
|
|
850
|
+
COUNT(*) FILTER (WHERE s.category = 'ai')::int AS ai_runs,
|
|
851
|
+
COUNT(*) FILTER (WHERE s.category = 'custom')::int AS custom_runs,
|
|
852
|
+
COUNT(*) FILTER (WHERE s.ended_at IS NULL AND s.started_at >= now() - (${TRACE_STUCK_AFTER_MS}::bigint * INTERVAL '1 millisecond'))::int AS running,
|
|
853
|
+
COUNT(*) FILTER (WHERE s.ended_at IS NULL AND s.started_at < now() - (${TRACE_STUCK_AFTER_MS}::bigint * INTERVAL '1 millisecond'))::int AS stuck,
|
|
854
|
+
COUNT(*) FILTER (WHERE s.duration_ms > ${TRACE_STUCK_AFTER_MS})::int AS anomalous,
|
|
855
|
+
COUNT(*) FILTER (WHERE s.status = 'ok')::int AS succeeded,
|
|
856
|
+
COUNT(*) FILTER (WHERE s.status = 'error')::int AS failed,
|
|
857
|
+
COALESCE(ROUND((COUNT(*) FILTER (WHERE s.status = 'error'))::numeric * 100 / NULLIF(COUNT(*), 0), 2), 0)::float AS error_rate,
|
|
858
|
+
(AVG(s.duration_ms) FILTER (WHERE s.duration_ms IS NOT NULL AND s.duration_ms <= ${TRACE_STUCK_AFTER_MS}))::float AS avg_duration_ms,
|
|
859
|
+
(percentile_cont(0.95) WITHIN GROUP (ORDER BY s.duration_ms) FILTER (WHERE s.duration_ms IS NOT NULL AND s.duration_ms <= ${TRACE_STUCK_AFTER_MS}))::float AS p95_duration_ms,
|
|
860
|
+
(percentile_cont(0.99) WITHIN GROUP (ORDER BY s.duration_ms) FILTER (WHERE s.duration_ms IS NOT NULL AND s.duration_ms <= ${TRACE_STUCK_AFTER_MS}))::float AS p99_duration_ms,
|
|
861
|
+
(array_agg(s.name ORDER BY s.started_at DESC, s.updated_at DESC))[1] AS latest_name,
|
|
862
|
+
(array_agg(s.category ORDER BY s.started_at DESC, s.updated_at DESC))[1] AS latest_category,
|
|
863
|
+
(array_agg(s.status ORDER BY s.started_at DESC, s.updated_at DESC))[1] AS latest_status,
|
|
864
|
+
(array_agg(s.started_at ORDER BY s.started_at DESC, s.updated_at DESC))[1] AS latest_started_at,
|
|
865
|
+
(array_agg(s.ended_at ORDER BY s.started_at DESC, s.updated_at DESC))[1] AS latest_ended_at,
|
|
866
|
+
(array_agg(s.duration_ms ORDER BY s.started_at DESC, s.updated_at DESC))[1] AS latest_duration_ms
|
|
867
|
+
FROM logging.trace_spans s
|
|
868
|
+
WHERE ${where}
|
|
869
|
+
GROUP BY s.source
|
|
870
|
+
)
|
|
871
|
+
SELECT *
|
|
872
|
+
FROM grouped
|
|
873
|
+
ORDER BY
|
|
874
|
+
(latest_ended_at IS NULL) DESC,
|
|
875
|
+
(latest_status = 'error') DESC,
|
|
876
|
+
error_rate DESC,
|
|
877
|
+
p99_duration_ms DESC NULLS LAST,
|
|
878
|
+
runs DESC,
|
|
879
|
+
source ASC
|
|
880
|
+
`;
|
|
881
|
+
return rows.map((row) => ({ ...mapSourceGroupRow(row), stuck: stuckBySource.get(row.source) ?? 0 }));
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const events = async (params: { traceId: string; spanId?: string; limit?: number }): Promise<TraceEvent[]> => {
|
|
885
|
+
const limit = normalizeLimit(params.limit);
|
|
886
|
+
const rows = await sql<DbTraceEventRow[]>`
|
|
887
|
+
SELECT id, trace_id, span_id, name, severity, attributes, body, occurred_at
|
|
888
|
+
FROM logging.trace_events
|
|
889
|
+
WHERE trace_id = ${params.traceId}
|
|
890
|
+
AND (${params.spanId ?? null}::text IS NULL OR span_id = ${params.spanId ?? null})
|
|
891
|
+
ORDER BY occurred_at ASC, id ASC
|
|
892
|
+
LIMIT ${limit}
|
|
893
|
+
`;
|
|
894
|
+
return rows.map(mapEventRow);
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const sources = async (): Promise<string[]> => {
|
|
898
|
+
const rows = await sql<{ source: string }[]>`
|
|
899
|
+
SELECT DISTINCT source FROM logging.trace_spans ORDER BY source
|
|
900
|
+
`;
|
|
901
|
+
return rows.map((row) => row.source);
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
const summary = async (options?: { sinceHours?: number }): Promise<TraceSummary> => {
|
|
905
|
+
const sinceHours = normalizeSinceHours(options?.sinceHours) ?? 24;
|
|
906
|
+
const [row] = await sql<
|
|
907
|
+
{
|
|
908
|
+
total: number;
|
|
909
|
+
total_window: number;
|
|
910
|
+
running: number;
|
|
911
|
+
succeeded_window: number;
|
|
912
|
+
failed_window: number;
|
|
913
|
+
sources: number;
|
|
914
|
+
avg_duration_ms: number | string | null;
|
|
915
|
+
p95_duration_ms: number | string | null;
|
|
916
|
+
}[]
|
|
917
|
+
>`
|
|
918
|
+
SELECT
|
|
919
|
+
(SELECT COUNT(*)::int FROM logging.trace_spans) AS total,
|
|
920
|
+
(SELECT COUNT(*)::int FROM logging.trace_spans WHERE started_at >= now() - (${sinceHours}::int * INTERVAL '1 hour')) AS total_window,
|
|
921
|
+
(SELECT COUNT(*)::int FROM logging.trace_spans WHERE ended_at IS NULL) AS running,
|
|
922
|
+
(SELECT COUNT(*)::int FROM logging.trace_spans WHERE status = 'ok' AND started_at >= now() - (${sinceHours}::int * INTERVAL '1 hour')) AS succeeded_window,
|
|
923
|
+
(SELECT COUNT(*)::int FROM logging.trace_spans WHERE status = 'error' AND started_at >= now() - (${sinceHours}::int * INTERVAL '1 hour')) AS failed_window,
|
|
924
|
+
(SELECT COUNT(*)::int FROM (SELECT DISTINCT source FROM logging.trace_spans) s) AS sources,
|
|
925
|
+
(SELECT AVG(duration_ms)::float FROM logging.trace_spans WHERE duration_ms IS NOT NULL AND started_at >= now() - (${sinceHours}::int * INTERVAL '1 hour')) AS avg_duration_ms,
|
|
926
|
+
(
|
|
927
|
+
SELECT percentile_cont(0.95) WITHIN GROUP (ORDER BY duration_ms)::float
|
|
928
|
+
FROM logging.trace_spans
|
|
929
|
+
WHERE duration_ms IS NOT NULL AND started_at >= now() - (${sinceHours}::int * INTERVAL '1 hour')
|
|
930
|
+
) AS p95_duration_ms
|
|
931
|
+
`;
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
total: row?.total ?? 0,
|
|
935
|
+
totalWindow: row?.total_window ?? 0,
|
|
936
|
+
running: row?.running ?? 0,
|
|
937
|
+
succeededWindow: row?.succeeded_window ?? 0,
|
|
938
|
+
failedWindow: row?.failed_window ?? 0,
|
|
939
|
+
sources: row?.sources ?? 0,
|
|
940
|
+
avgDurationMs: toNumberOrNull(row?.avg_duration_ms),
|
|
941
|
+
p95DurationMs: toNumberOrNull(row?.p95_duration_ms),
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
const cleanup = async (options: { days: number; source?: string }): Promise<number> => {
|
|
946
|
+
const days = Math.max(1, Math.trunc(options.days));
|
|
947
|
+
const result = await sql`
|
|
948
|
+
DELETE FROM logging.trace_spans
|
|
949
|
+
WHERE ended_at IS NOT NULL
|
|
950
|
+
AND ended_at < now() - (${days}::int * INTERVAL '1 day')
|
|
951
|
+
AND (${options.source ?? null}::text IS NULL OR source = ${options.source ?? null})
|
|
952
|
+
`;
|
|
953
|
+
return result.count;
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
// ==========================
|
|
957
|
+
// Sync observe adapter
|
|
958
|
+
// ==========================
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Sync v6 emits ids, keys and statuses only — never handler results — so a
|
|
962
|
+
* run span carries the run identity and outcome. A handler that wants a
|
|
963
|
+
* summary attaches it itself with `trace.end({ spanKey, summary })`, using
|
|
964
|
+
* `trace.syncSpanKey(kind, resource, runId)` for the same span.
|
|
965
|
+
*
|
|
966
|
+
* Topic consumer runs are traced only when they fail: a successful first
|
|
967
|
+
* attempt writes nothing. Telemetry consumers process one event per request,
|
|
968
|
+
* and two span writes per event would multiply the write load and saturate
|
|
969
|
+
* the bounded observer buffer, dropping the process's other Sync diagnostics.
|
|
970
|
+
* A retry materializes the run as an error span, so a later successful
|
|
971
|
+
* attempt is traced too (one extra write only for runs that retried) and
|
|
972
|
+
* closes that span as `ok` instead of leaving it `error` forever. Topic
|
|
973
|
+
* redeliveries carry no run id and are dropped as well; the retry span
|
|
974
|
+
* already records them.
|
|
975
|
+
*/
|
|
976
|
+
|
|
977
|
+
type SyncRunKind = "queue" | "job" | "topic" | "scheduler" | "pump";
|
|
978
|
+
|
|
979
|
+
const SYNC_CATEGORY: Record<SyncRunKind, TraceCategory> = {
|
|
980
|
+
queue: "job",
|
|
981
|
+
job: "job",
|
|
982
|
+
topic: "sync",
|
|
983
|
+
scheduler: "schedule",
|
|
984
|
+
pump: "backfill",
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
/** Span key of one Sync run. Topic handlers also pass their independent consumer name. */
|
|
988
|
+
const syncSpanKey = (kind: SyncRunKind, resource: string, runId: string, consumer?: string): string =>
|
|
989
|
+
kind === "topic" && consumer !== undefined
|
|
990
|
+
? `sync:topic:${JSON.stringify([resource, runId, consumer])}`
|
|
991
|
+
: `sync:${kind}:${resource}:${runId}`;
|
|
992
|
+
|
|
993
|
+
const isSyncRunKind = (kind: string): kind is SyncRunKind => kind in SYNC_CATEGORY;
|
|
994
|
+
|
|
995
|
+
const TRACED_SYNC_EVENT_TYPES = new Set([
|
|
996
|
+
"handler_started",
|
|
997
|
+
"handler_settled",
|
|
998
|
+
"dead_letter",
|
|
999
|
+
"redelivery",
|
|
1000
|
+
"pump_run_started",
|
|
1001
|
+
"pump_run_settled",
|
|
1002
|
+
]);
|
|
1003
|
+
|
|
1004
|
+
/** Whether the adapter records this event at all (see the topic policy above). */
|
|
1005
|
+
const isTracedSyncEvent = (event: SyncEvent): boolean => {
|
|
1006
|
+
if (!event.resource || !event.kind || !isSyncRunKind(event.kind) || !TRACED_SYNC_EVENT_TYPES.has(event.type)) return false;
|
|
1007
|
+
if (event.kind !== "topic") return true;
|
|
1008
|
+
if (event.type === "handler_started" || event.type === "redelivery") return false;
|
|
1009
|
+
if (event.type !== "handler_settled") return true;
|
|
1010
|
+
const status = event.detail?.status;
|
|
1011
|
+
if (status === "retry" || status === "dead_letter") return true;
|
|
1012
|
+
const attempt = event.detail?.attempt;
|
|
1013
|
+
return status === "success" && typeof attempt === "number" && attempt > 1;
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
const detailString = (detail: Record<string, unknown>, key: string): string | undefined => {
|
|
1017
|
+
const value = detail[key];
|
|
1018
|
+
return typeof value === "string" ? value : undefined;
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const detailNumber = (detail: Record<string, unknown>, key: string): number | undefined => {
|
|
1022
|
+
const value = detail[key];
|
|
1023
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
const detailAttributes = (detail: Record<string, unknown>): TraceAttributes => {
|
|
1027
|
+
const out: TraceAttributes = {};
|
|
1028
|
+
for (const [key, value] of Object.entries(detail)) {
|
|
1029
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1030
|
+
out[`sync.${key}`] = value;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return out;
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
/** The app hosting this process; Sync events carry no app name. */
|
|
1037
|
+
const processAppId = (): string | undefined => process.env.APP_ID?.trim() || undefined;
|
|
1038
|
+
|
|
1039
|
+
const traceSyncEvent = async (event: SyncEvent, application?: string): Promise<void> => {
|
|
1040
|
+
const resource = event.resource;
|
|
1041
|
+
const kind = event.kind;
|
|
1042
|
+
if (!resource || !kind || !isSyncRunKind(kind) || !isTracedSyncEvent(event)) return;
|
|
1043
|
+
const detail: Record<string, unknown> = event.detail ?? {};
|
|
1044
|
+
const appId = application ?? processAppId();
|
|
1045
|
+
const category = SYNC_CATEGORY[kind];
|
|
1046
|
+
const attributes: TraceAttributes = {
|
|
1047
|
+
"sync.kind": kind,
|
|
1048
|
+
"sync.resource": resource,
|
|
1049
|
+
...detailAttributes(detail),
|
|
1050
|
+
...(event.error ? { "sync.error": event.error } : {}),
|
|
1051
|
+
};
|
|
1052
|
+
// Topic dead letters carry the consumer instead of the run key.
|
|
1053
|
+
const key = detailString(detail, "key") ?? (kind === "topic" ? detailString(detail, "consumer") : undefined);
|
|
1054
|
+
const consumer = kind === "topic" ? key : undefined;
|
|
1055
|
+
// Per-schedule and per-consumer names stay readable; job keys are unbounded.
|
|
1056
|
+
const name = (kind === "scheduler" || kind === "topic") && key ? key : resource;
|
|
1057
|
+
const base = { name, source: resource, appId, category, kind: "consumer" as const };
|
|
1058
|
+
|
|
1059
|
+
switch (event.type) {
|
|
1060
|
+
case "handler_started": {
|
|
1061
|
+
const id = detailString(detail, "id");
|
|
1062
|
+
if (!id) return;
|
|
1063
|
+
await start({ ...base, spanKey: syncSpanKey(kind, resource, id, consumer), attributes, startedAt: event.at }, true);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
case "handler_settled": {
|
|
1067
|
+
const id = detailString(detail, "id");
|
|
1068
|
+
if (!id) return;
|
|
1069
|
+
const spanKey = syncSpanKey(kind, resource, id, consumer);
|
|
1070
|
+
const status = detailString(detail, "status");
|
|
1071
|
+
const attempt = detailNumber(detail, "attempt");
|
|
1072
|
+
const durationMs = detailNumber(detail, "durationMs") ?? 0;
|
|
1073
|
+
if (status === "retry") {
|
|
1074
|
+
// Topic runs have no started span: materialize the failed run first.
|
|
1075
|
+
if (kind === "topic") {
|
|
1076
|
+
await complete(
|
|
1077
|
+
{
|
|
1078
|
+
...base,
|
|
1079
|
+
spanKey,
|
|
1080
|
+
status: "error",
|
|
1081
|
+
statusMessage: `Retry scheduled after ${attempt ?? "?"} attempt(s)`,
|
|
1082
|
+
attributes,
|
|
1083
|
+
startedAt: event.at.getTime() - durationMs,
|
|
1084
|
+
endedAt: event.at,
|
|
1085
|
+
},
|
|
1086
|
+
true,
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
await record(
|
|
1090
|
+
{
|
|
1091
|
+
...base,
|
|
1092
|
+
spanKey,
|
|
1093
|
+
event: "sync.retry",
|
|
1094
|
+
severity: "warn",
|
|
1095
|
+
status: "error",
|
|
1096
|
+
attributes,
|
|
1097
|
+
occurredAt: event.at,
|
|
1098
|
+
},
|
|
1099
|
+
true,
|
|
1100
|
+
);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
await complete(
|
|
1104
|
+
{
|
|
1105
|
+
...base,
|
|
1106
|
+
spanKey,
|
|
1107
|
+
status: status === "success" ? "ok" : "error",
|
|
1108
|
+
statusMessage: status === "success" ? undefined : `Dead-lettered after ${attempt ?? "?"} attempt(s)`,
|
|
1109
|
+
attributes,
|
|
1110
|
+
startedAt: event.at.getTime() - durationMs,
|
|
1111
|
+
endedAt: event.at,
|
|
1112
|
+
},
|
|
1113
|
+
true,
|
|
1114
|
+
);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
case "dead_letter": {
|
|
1118
|
+
const id = detailString(detail, "messageId") ?? detailString(detail, "eventId");
|
|
1119
|
+
if (!id) return;
|
|
1120
|
+
const spanKey = syncSpanKey(kind, resource, id, consumer);
|
|
1121
|
+
const reason = detailString(detail, "reason") ?? "dead letter";
|
|
1122
|
+
// A message can be dead-lettered without a handler run (attempts
|
|
1123
|
+
// exhausted on delivery, undecodable payload): the upsert closes the
|
|
1124
|
+
// span either way, and the event records the reason.
|
|
1125
|
+
await complete(
|
|
1126
|
+
{
|
|
1127
|
+
...base,
|
|
1128
|
+
spanKey,
|
|
1129
|
+
status: "error",
|
|
1130
|
+
statusMessage: reason,
|
|
1131
|
+
attributes,
|
|
1132
|
+
startedAt: event.at,
|
|
1133
|
+
endedAt: event.at,
|
|
1134
|
+
},
|
|
1135
|
+
true,
|
|
1136
|
+
);
|
|
1137
|
+
await record({ context: newContext(spanKey), event: "sync.dead_letter", severity: "error", attributes, occurredAt: event.at });
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
case "redelivery": {
|
|
1141
|
+
// Carries no run id: recorded as its own zero-length span next to the runs.
|
|
1142
|
+
await record({ ...base, category: "sync", event: "sync.redelivery", severity: "warn", attributes, occurredAt: event.at });
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
case "pump_run_started": {
|
|
1146
|
+
if (!key) return;
|
|
1147
|
+
await start({ ...base, spanKey: syncSpanKey("pump", resource, key), attributes, startedAt: event.at }, true);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
case "pump_run_settled": {
|
|
1151
|
+
if (!key) return;
|
|
1152
|
+
const status = detailString(detail, "status");
|
|
1153
|
+
const durationMs = detailNumber(detail, "durationMs") ?? 0;
|
|
1154
|
+
await complete(
|
|
1155
|
+
{
|
|
1156
|
+
...base,
|
|
1157
|
+
spanKey: syncSpanKey("pump", resource, key),
|
|
1158
|
+
status: status === "failed" ? "error" : "ok",
|
|
1159
|
+
statusMessage: detailString(detail, "error"),
|
|
1160
|
+
attributes,
|
|
1161
|
+
summary: { status, dispatched: detailNumber(detail, "dispatched"), failureCount: detailNumber(detail, "failureCount") },
|
|
1162
|
+
startedAt: event.at.getTime() - durationMs,
|
|
1163
|
+
endedAt: event.at,
|
|
1164
|
+
},
|
|
1165
|
+
true,
|
|
1166
|
+
);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
default:
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// Match Sync's event subscription buffer budget. Observer overload drops
|
|
1175
|
+
// diagnostics rather than retaining unbounded SQL promises or delaying jobs.
|
|
1176
|
+
const SYNC_TRACE_PENDING_LIMIT = 1024;
|
|
1177
|
+
const pendingSyncWrites = new Map<string, Promise<void>>();
|
|
1178
|
+
let pendingSyncWriteCount = 0;
|
|
1179
|
+
let reportedSyncOverflow = false;
|
|
1180
|
+
|
|
1181
|
+
/** Ordered per-run, bounded, non-blocking adapter for createSync({ observe }). */
|
|
1182
|
+
export const observeSyncEvent = (event: SyncEvent, application?: string): void => {
|
|
1183
|
+
if (!event.resource || !event.kind || !isSyncRunKind(event.kind) || !isTracedSyncEvent(event)) return;
|
|
1184
|
+
if (pendingSyncWriteCount >= SYNC_TRACE_PENDING_LIMIT) {
|
|
1185
|
+
if (!reportedSyncOverflow) console.error("[logging:trace] sync trace buffer full; dropping diagnostics");
|
|
1186
|
+
reportedSyncOverflow = true;
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const detail = event.detail ?? {};
|
|
1190
|
+
const id =
|
|
1191
|
+
detailString(detail, "id") ?? detailString(detail, "messageId") ?? detailString(detail, "eventId") ?? detailString(detail, "key") ?? "";
|
|
1192
|
+
const consumer = event.kind === "topic" ? (detailString(detail, "key") ?? detailString(detail, "consumer")) : undefined;
|
|
1193
|
+
const key = syncSpanKey(event.kind, event.resource, id, consumer);
|
|
1194
|
+
pendingSyncWriteCount++;
|
|
1195
|
+
const write = (pendingSyncWrites.get(key) ?? Promise.resolve())
|
|
1196
|
+
.then(() => traceSyncEvent(event, application))
|
|
1197
|
+
.catch((error: unknown) => {
|
|
1198
|
+
console.error("[logging:trace] sync event failed:", error instanceof Error ? error.message : String(error));
|
|
1199
|
+
})
|
|
1200
|
+
.finally(() => {
|
|
1201
|
+
pendingSyncWriteCount--;
|
|
1202
|
+
if (pendingSyncWrites.get(key) === write) pendingSyncWrites.delete(key);
|
|
1203
|
+
if (pendingSyncWriteCount === 0) reportedSyncOverflow = false;
|
|
1204
|
+
});
|
|
1205
|
+
pendingSyncWrites.set(key, write);
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
/** Flush diagnostics after Sync workers stop producing events during shutdown. */
|
|
1209
|
+
export const flushSyncTraceEvents = async (): Promise<void> => {
|
|
1210
|
+
while (pendingSyncWrites.size > 0) await Promise.all(pendingSyncWrites.values());
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
/** Awaitable form of {@link observeSyncEvent} for tests. */
|
|
1214
|
+
export { traceSyncEvent };
|
|
1215
|
+
|
|
1216
|
+
export const trace = {
|
|
1217
|
+
start,
|
|
1218
|
+
complete,
|
|
1219
|
+
record,
|
|
1220
|
+
end,
|
|
1221
|
+
withSpan,
|
|
1222
|
+
list,
|
|
1223
|
+
getSpan,
|
|
1224
|
+
stats,
|
|
1225
|
+
sourceGroups,
|
|
1226
|
+
events,
|
|
1227
|
+
sources,
|
|
1228
|
+
summary,
|
|
1229
|
+
cleanup,
|
|
1230
|
+
syncSpanKey,
|
|
1231
|
+
};
|