pipework 0.251.0 → 0.252.1
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/CHANGELOG.md +11 -0
- package/dist/core/config/load.d.ts +2 -0
- package/dist/core/config/load.d.ts.map +1 -1
- package/dist/core/config/load.js +1 -0
- package/dist/core/config/load.js.map +1 -1
- package/dist/core/config/namespace.d.ts +1 -0
- package/dist/core/config/namespace.d.ts.map +1 -1
- package/dist/core/config/schema.d.ts +2 -0
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +1 -0
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/surface/types.d.ts +1 -0
- package/dist/core/surface/types.d.ts.map +1 -1
- package/dist/data/db/namespace.d.ts +3 -0
- package/dist/data/db/namespace.d.ts.map +1 -1
- package/dist/data/db/namespace.js +3 -0
- package/dist/data/db/namespace.js.map +1 -1
- package/dist/data/db/pool.d.ts.map +1 -1
- package/dist/data/db/pool.js +6 -1
- package/dist/data/db/pool.js.map +1 -1
- package/dist/data/db/query-label.d.ts +5 -0
- package/dist/data/db/query-label.d.ts.map +1 -0
- package/dist/data/db/query-label.js +11 -0
- package/dist/data/db/query-label.js.map +1 -0
- package/dist/data/db/query-watch.d.ts +21 -0
- package/dist/data/db/query-watch.d.ts.map +1 -0
- package/dist/data/db/query-watch.js +90 -0
- package/dist/data/db/query-watch.js.map +1 -0
- package/dist/data/query/postgres-js/driver.js +1 -1
- package/dist/data/query/postgres-js/driver.js.map +1 -1
- package/dist/data/query/postgres-js/session.d.ts +5 -1
- package/dist/data/query/postgres-js/session.d.ts.map +1 -1
- package/dist/data/query/postgres-js/session.js +16 -11
- package/dist/data/query/postgres-js/session.js.map +1 -1
- package/dist/data/query/utils.d.ts +2 -0
- package/dist/data/query/utils.d.ts.map +1 -1
- package/dist/data/query/utils.js.map +1 -1
- package/dist/infra/credits/schema.d.ts +1 -1
- package/dist/test/plugin.d.ts.map +1 -1
- package/dist/test/plugin.js +10 -4
- package/dist/test/plugin.js.map +1 -1
- package/package.json +4 -3
- package/src/async/jobs/abortable-delay.ts +18 -0
- package/src/async/jobs/claims.ts +52 -0
- package/src/async/jobs/cron.ts +179 -0
- package/src/async/jobs/errors.ts +56 -0
- package/src/async/jobs/exclusion.ts +117 -0
- package/src/async/jobs/execute.ts +219 -0
- package/src/async/jobs/index.ts +10 -0
- package/src/async/jobs/listener.ts +135 -0
- package/src/async/jobs/namespace.ts +20 -0
- package/src/async/jobs/queue-door.ts +44 -0
- package/src/async/jobs/queue.ts +1012 -0
- package/src/async/jobs/run-script.ts +67 -0
- package/src/async/jobs/scheduler.ts +218 -0
- package/src/async/jobs/schedules-table.ts +37 -0
- package/src/async/jobs/serialize-error.ts +48 -0
- package/src/async/jobs/table.ts +191 -0
- package/src/async/pipeline/define.ts +53 -0
- package/src/async/pipeline/errors.ts +35 -0
- package/src/async/pipeline/execute.ts +53 -0
- package/src/async/pipeline/index.ts +13 -0
- package/src/async/pipeline/namespace.ts +12 -0
- package/src/async/pipeline/resume.ts +67 -0
- package/src/async/pipeline/run-steps.ts +98 -0
- package/src/async/pipeline/types.ts +57 -0
- package/src/async/state-machine/define.ts +64 -0
- package/src/async/state-machine/errors.ts +29 -0
- package/src/async/state-machine/index.ts +3 -0
- package/src/async/state-machine/namespace.ts +12 -0
- package/src/async/state-machine/transition.ts +90 -0
- package/src/async/state-machine/types.ts +37 -0
- package/src/async/steps/errors.ts +31 -0
- package/src/async/steps/graph.ts +1214 -0
- package/src/async/steps/hash.ts +40 -0
- package/src/async/steps/index.ts +7 -0
- package/src/async/steps/namespace.ts +7 -0
- package/src/async/steps/table.ts +97 -0
- package/src/async/steps/types.ts +318 -0
- package/src/audit/chain/index.ts +9 -0
- package/src/audit/chain/install.ts +113 -0
- package/src/audit/chain/retire.ts +52 -0
- package/src/audit/chain/verify.ts +257 -0
- package/src/audit/query/cursor.ts +37 -0
- package/src/audit/query/index.ts +9 -0
- package/src/audit/query/query.ts +223 -0
- package/src/audit/schema/audit-record-fields.ts +68 -0
- package/src/audit/schema/audit-record.ts +23 -0
- package/src/auth/auth/audit.ts +31 -0
- package/src/auth/auth/chain.ts +84 -0
- package/src/auth/auth/cookie.ts +136 -0
- package/src/auth/auth/email-links.ts +311 -0
- package/src/auth/auth/guard.ts +128 -0
- package/src/auth/auth/index.ts +18 -0
- package/src/auth/auth/multi-org.ts +337 -0
- package/src/auth/auth/namespace.ts +34 -0
- package/src/auth/auth/parse-expiry.ts +13 -0
- package/src/auth/auth/passwords.ts +110 -0
- package/src/auth/auth/schema.ts +110 -0
- package/src/auth/auth/sessions.ts +565 -0
- package/src/auth/auth/sign-in.ts +156 -0
- package/src/auth/auth/signing-keys.ts +69 -0
- package/src/auth/auth/types.ts +26 -0
- package/src/auth/auth/uuid.ts +10 -0
- package/src/auth/rbac/cache.ts +55 -0
- package/src/auth/rbac/check.ts +30 -0
- package/src/auth/rbac/create.ts +35 -0
- package/src/auth/rbac/index.ts +5 -0
- package/src/auth/rbac/middleware.ts +20 -0
- package/src/auth/rbac/namespace.ts +15 -0
- package/src/auth/rbac/resolve.ts +31 -0
- package/src/auth/rbac/schema.sql +31 -0
- package/src/auth/rbac/seed.ts +67 -0
- package/src/auth/rbac/tables.ts +32 -0
- package/src/auth/rbac/types.ts +65 -0
- package/src/auth/tenant/extract.ts +33 -0
- package/src/auth/tenant/in-tenant.ts +49 -0
- package/src/auth/tenant/index.ts +7 -0
- package/src/auth/tenant/namespace.ts +23 -0
- package/src/auth/tenant/propagate.ts +20 -0
- package/src/auth/tenant/rls.ts +120 -0
- package/src/auth/tenant/scope.ts +33 -0
- package/src/auth/tenant/scoped-db.ts +581 -0
- package/src/auth/tenant/types.ts +14 -0
- package/src/auth/tenant/validate.ts +27 -0
- package/src/cli/commands/add.ts +158 -0
- package/src/cli/commands/alerts.ts +98 -0
- package/src/cli/commands/check.ts +1418 -0
- package/src/cli/commands/codemod.ts +78 -0
- package/src/cli/commands/db.ts +356 -0
- package/src/cli/commands/dev.ts +101 -0
- package/src/cli/commands/generate.ts +75 -0
- package/src/cli/commands/init.ts +36 -0
- package/src/cli/commands/install.ts +28 -0
- package/src/cli/commands/lint.ts +62 -0
- package/src/cli/commands/migrate.ts +132 -0
- package/src/cli/commands/openapi.ts +63 -0
- package/src/cli/commands/remove.ts +72 -0
- package/src/cli/commands/run.ts +50 -0
- package/src/cli/commands/scope.ts +54 -0
- package/src/cli/commands/serve.ts +68 -0
- package/src/cli/commands/shard.ts +31 -0
- package/src/cli/commands/shared/config-pipework-version.ts +28 -0
- package/src/cli/commands/shared/find-config.ts +22 -0
- package/src/cli/commands/shared/load-config.ts +35 -0
- package/src/cli/commands/shared/pnpm-install.ts +95 -0
- package/src/cli/commands/shared/report-removed.ts +8 -0
- package/src/cli/commands/sync.ts +32 -0
- package/src/cli/commands/test.ts +307 -0
- package/src/cli/commands/upgrade.ts +48 -0
- package/src/cli/dev-bootstrap.ts +24 -0
- package/src/cli/errors.ts +10 -0
- package/src/cli/index.ts +303 -0
- package/src/cli/output.ts +17 -0
- package/src/cli/port.ts +15 -0
- package/src/cli/surface-kinds.ts +20 -0
- package/src/client/index.ts +4 -0
- package/src/client/poll.ts +130 -0
- package/src/client/report-fault.ts +119 -0
- package/src/core/config/app-pool.ts +19 -0
- package/src/core/config/aws-env.ts +29 -0
- package/src/core/config/base-dir.ts +24 -0
- package/src/core/config/discover.ts +64 -0
- package/src/core/config/env-resolve.ts +87 -0
- package/src/core/config/env-types.ts +116 -0
- package/src/core/config/env.ts +55 -0
- package/src/core/config/errors.ts +9 -0
- package/src/core/config/import-config.ts +40 -0
- package/src/core/config/index.ts +8 -0
- package/src/core/config/load.ts +417 -0
- package/src/core/config/namespace.ts +14 -0
- package/src/core/config/resolve.ts +195 -0
- package/src/core/config/schema.ts +488 -0
- package/src/core/config/statement-observer.ts +48 -0
- package/src/core/config/ts-inline-hooks.ts +75 -0
- package/src/core/config/ts-register.ts +4 -0
- package/src/core/config/ts-resolve-hooks.ts +101 -0
- package/src/core/errors/base.ts +49 -0
- package/src/core/errors/http.ts +146 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/namespace.ts +31 -0
- package/src/core/lifecycle/drain.ts +33 -0
- package/src/core/lifecycle/health.ts +72 -0
- package/src/core/lifecycle/index.ts +4 -0
- package/src/core/lifecycle/namespace.ts +7 -0
- package/src/core/lifecycle/startup.ts +102 -0
- package/src/core/lifecycle/types.ts +36 -0
- package/src/core/logging/context.ts +60 -0
- package/src/core/logging/error-chain.ts +68 -0
- package/src/core/logging/index.ts +5 -0
- package/src/core/logging/logger.ts +65 -0
- package/src/core/logging/namespace.ts +16 -0
- package/src/core/logging/proxy.ts +22 -0
- package/src/core/logging/redact-match.ts +24 -0
- package/src/core/logging/types.ts +47 -0
- package/src/core/pipework.ts +566 -0
- package/src/core/runtime/broker.ts +69 -0
- package/src/core/surface/http.ts +133 -0
- package/src/core/surface/index.ts +11 -0
- package/src/core/surface/job-timeout.ts +40 -0
- package/src/core/surface/namespace.ts +13 -0
- package/src/core/surface/script.ts +33 -0
- package/src/core/surface/start.ts +142 -0
- package/src/core/surface/types.ts +225 -0
- package/src/core/surface/validate.ts +88 -0
- package/src/core/surface/worker.ts +528 -0
- package/src/core/validation/connectivity.ts +49 -0
- package/src/core/validation/domain.ts +99 -0
- package/src/core/validation/index.ts +5 -0
- package/src/core/validation/queue-tables.ts +312 -0
- package/src/core/validation/rls-role.ts +138 -0
- package/src/core/validation/startup.ts +51 -0
- package/src/core/validation/surfaces.ts +65 -0
- package/src/core/validation/types.ts +25 -0
- package/src/data/db/advisory-lock.ts +55 -0
- package/src/data/db/aggregates.ts +172 -0
- package/src/data/db/array-elements.ts +111 -0
- package/src/data/db/bulk-set.ts +53 -0
- package/src/data/db/col/index.ts +14 -0
- package/src/data/db/composed.ts +53 -0
- package/src/data/db/db.ts +276 -0
- package/src/data/db/excluded.ts +16 -0
- package/src/data/db/expressions.ts +473 -0
- package/src/data/db/filter.ts +90 -0
- package/src/data/db/function-source.ts +114 -0
- package/src/data/db/identifiers.ts +16 -0
- package/src/data/db/idx/index.ts +9 -0
- package/src/data/db/in-list.ts +49 -0
- package/src/data/db/index.ts +9 -0
- package/src/data/db/insert-split.ts +137 -0
- package/src/data/db/instrumentation.ts +82 -0
- package/src/data/db/maintenance.ts +28 -0
- package/src/data/db/namespace.ts +107 -0
- package/src/data/db/owner-labels.ts +9 -0
- package/src/data/db/pool.ts +187 -0
- package/src/data/db/query-label.ts +13 -0
- package/src/data/db/query-watch.ts +116 -0
- package/src/data/db/raw-client-warning.ts +37 -0
- package/src/data/db/readonly.ts +23 -0
- package/src/data/db/reservation.ts +151 -0
- package/src/data/db/schema-features.ts +14 -0
- package/src/data/db/serializable.ts +83 -0
- package/src/data/db/sql.ts +1 -0
- package/src/data/db/timeouts.ts +76 -0
- package/src/data/db/types.ts +18 -0
- package/src/data/db/unbound.ts +33 -0
- package/src/data/db/values.ts +29 -0
- package/src/data/domain/brand.ts +15 -0
- package/src/data/domain/build-factory.ts +193 -0
- package/src/data/domain/build-schema.ts +161 -0
- package/src/data/domain/build-table.ts +355 -0
- package/src/data/domain/define.ts +239 -0
- package/src/data/domain/external-registry.ts +34 -0
- package/src/data/domain/external.ts +77 -0
- package/src/data/domain/field.ts +279 -0
- package/src/data/domain/index.ts +13 -0
- package/src/data/domain/naming.ts +6 -0
- package/src/data/domain/projections.ts +57 -0
- package/src/data/domain/trace-safe-projection.ts +31 -0
- package/src/data/domain/traits.ts +108 -0
- package/src/data/domain/types.ts +289 -0
- package/src/data/jsonb/composed.ts +18 -0
- package/src/data/jsonb/index.ts +1 -0
- package/src/data/jsonb/namespace.ts +30 -0
- package/src/data/jsonb/query.ts +86 -0
- package/src/data/migrate/append-only-generate.ts +47 -0
- package/src/data/migrate/applied.ts +47 -0
- package/src/data/migrate/apply.ts +309 -0
- package/src/data/migrate/check.ts +119 -0
- package/src/data/migrate/diff.ts +357 -0
- package/src/data/migrate/door-generate.ts +130 -0
- package/src/data/migrate/framework-rls.ts +58 -0
- package/src/data/migrate/generate.ts +201 -0
- package/src/data/migrate/history.ts +215 -0
- package/src/data/migrate/index.ts +9 -0
- package/src/data/migrate/internal-definitions.ts +175 -0
- package/src/data/migrate/post-process.ts +73 -0
- package/src/data/migrate/pragma.ts +73 -0
- package/src/data/migrate/provision-framework.ts +147 -0
- package/src/data/migrate/rls-generate.ts +61 -0
- package/src/data/migrate/run.ts +124 -0
- package/src/data/migrate/safety.ts +167 -0
- package/src/data/migrate/snapshot.ts +368 -0
- package/src/data/migrate/sql-emit.ts +392 -0
- package/src/data/migrate/statements.ts +312 -0
- package/src/data/query/alias.ts +126 -0
- package/src/data/query/batch.ts +8 -0
- package/src/data/query/cache/core/cache.ts +78 -0
- package/src/data/query/cache/core/index.ts +1 -0
- package/src/data/query/cache/core/types.ts +29 -0
- package/src/data/query/cache/readme.md +230 -0
- package/src/data/query/casing.ts +76 -0
- package/src/data/query/collation.ts +11 -0
- package/src/data/query/column-builder.ts +370 -0
- package/src/data/query/column.ts +150 -0
- package/src/data/query/entity.ts +42 -0
- package/src/data/query/errors.ts +33 -0
- package/src/data/query/index.ts +14 -0
- package/src/data/query/logger.ts +47 -0
- package/src/data/query/operations.ts +45 -0
- package/src/data/query/pg-core/alias.ts +12 -0
- package/src/data/query/pg-core/checks.ts +32 -0
- package/src/data/query/pg-core/columns/all.ts +71 -0
- package/src/data/query/pg-core/columns/bigint.ts +110 -0
- package/src/data/query/pg-core/columns/bigserial.ts +128 -0
- package/src/data/query/pg-core/columns/boolean.ts +43 -0
- package/src/data/query/pg-core/columns/char.ts +85 -0
- package/src/data/query/pg-core/columns/cidr.ts +43 -0
- package/src/data/query/pg-core/columns/common.ts +366 -0
- package/src/data/query/pg-core/columns/custom.ts +232 -0
- package/src/data/query/pg-core/columns/date.common.ts +15 -0
- package/src/data/query/pg-core/columns/date.ts +112 -0
- package/src/data/query/pg-core/columns/double-precision.ts +55 -0
- package/src/data/query/pg-core/columns/enum.ts +201 -0
- package/src/data/query/pg-core/columns/index.ts +35 -0
- package/src/data/query/pg-core/columns/inet.ts +43 -0
- package/src/data/query/pg-core/columns/int.common.ts +57 -0
- package/src/data/query/pg-core/columns/integer.ts +53 -0
- package/src/data/query/pg-core/columns/interval.ts +83 -0
- package/src/data/query/pg-core/columns/json.ts +58 -0
- package/src/data/query/pg-core/columns/jsonb.ts +56 -0
- package/src/data/query/pg-core/columns/line.ts +117 -0
- package/src/data/query/pg-core/columns/macaddr.ts +43 -0
- package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
- package/src/data/query/pg-core/columns/numeric.ts +227 -0
- package/src/data/query/pg-core/columns/point.ts +127 -0
- package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
- package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
- package/src/data/query/pg-core/columns/real.ts +58 -0
- package/src/data/query/pg-core/columns/serial.ts +55 -0
- package/src/data/query/pg-core/columns/smallint.ts +53 -0
- package/src/data/query/pg-core/columns/smallserial.ts +60 -0
- package/src/data/query/pg-core/columns/text.ts +67 -0
- package/src/data/query/pg-core/columns/time.ts +72 -0
- package/src/data/query/pg-core/columns/timestamp.ts +163 -0
- package/src/data/query/pg-core/columns/uuid.ts +51 -0
- package/src/data/query/pg-core/columns/varchar.ts +89 -0
- package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
- package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
- package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
- package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
- package/src/data/query/pg-core/db.ts +674 -0
- package/src/data/query/pg-core/dialect.ts +1404 -0
- package/src/data/query/pg-core/expressions.ts +25 -0
- package/src/data/query/pg-core/foreign-keys.ts +130 -0
- package/src/data/query/pg-core/index.ts +21 -0
- package/src/data/query/pg-core/indexes.ts +276 -0
- package/src/data/query/pg-core/policies.ts +56 -0
- package/src/data/query/pg-core/primary-keys.ts +62 -0
- package/src/data/query/pg-core/query-builders/count.ts +86 -0
- package/src/data/query/pg-core/query-builders/delete.ts +293 -0
- package/src/data/query/pg-core/query-builders/index.ts +7 -0
- package/src/data/query/pg-core/query-builders/insert.ts +452 -0
- package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
- package/src/data/query/pg-core/query-builders/query.ts +157 -0
- package/src/data/query/pg-core/query-builders/raw.ts +49 -0
- package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
- package/src/data/query/pg-core/query-builders/select.ts +1346 -0
- package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
- package/src/data/query/pg-core/query-builders/update.ts +626 -0
- package/src/data/query/pg-core/roles.ts +41 -0
- package/src/data/query/pg-core/schema.ts +72 -0
- package/src/data/query/pg-core/sequence.ts +41 -0
- package/src/data/query/pg-core/session.ts +292 -0
- package/src/data/query/pg-core/subquery.ts +46 -0
- package/src/data/query/pg-core/table.ts +252 -0
- package/src/data/query/pg-core/unique-constraint.ts +73 -0
- package/src/data/query/pg-core/utils/array.ts +95 -0
- package/src/data/query/pg-core/utils/index.ts +1 -0
- package/src/data/query/pg-core/utils.ts +104 -0
- package/src/data/query/pg-core/view-base.ts +14 -0
- package/src/data/query/pg-core/view-common.ts +1 -0
- package/src/data/query/pg-core/view.ts +435 -0
- package/src/data/query/pg-core/with-builder.ts +41 -0
- package/src/data/query/postgres-js/README.md +46 -0
- package/src/data/query/postgres-js/array-parser.ts +45 -0
- package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
- package/src/data/query/postgres-js/driver.ts +163 -0
- package/src/data/query/postgres-js/index.ts +2 -0
- package/src/data/query/postgres-js/session.ts +234 -0
- package/src/data/query/primary-key.ts +11 -0
- package/src/data/query/query-builders/query-builder.ts +19 -0
- package/src/data/query/query-builders/select.types.ts +175 -0
- package/src/data/query/query-promise.ts +35 -0
- package/src/data/query/relations.ts +725 -0
- package/src/data/query/runnable-query.ts +12 -0
- package/src/data/query/selection-proxy.ts +121 -0
- package/src/data/query/session.ts +8 -0
- package/src/data/query/sql/expressions/conditions.ts +741 -0
- package/src/data/query/sql/expressions/index.ts +2 -0
- package/src/data/query/sql/expressions/select.ts +45 -0
- package/src/data/query/sql/functions/aggregate.ts +129 -0
- package/src/data/query/sql/functions/index.ts +2 -0
- package/src/data/query/sql/functions/vector.ts +178 -0
- package/src/data/query/sql/index.ts +3 -0
- package/src/data/query/sql/sql.ts +794 -0
- package/src/data/query/subquery.ts +53 -0
- package/src/data/query/table.ts +208 -0
- package/src/data/query/table.utils.ts +2 -0
- package/src/data/query/tracing-utils.ts +3 -0
- package/src/data/query/tracing.ts +11 -0
- package/src/data/query/utils.ts +357 -0
- package/src/data/query/version.ts +4 -0
- package/src/data/query/view-common.ts +1 -0
- package/src/data/schema/branded.ts +13 -0
- package/src/data/schema/generated/coerce.ts +5 -0
- package/src/data/schema/generated/combinator.ts +6 -0
- package/src/data/schema/generated/encoding.ts +5 -0
- package/src/data/schema/generated/error.ts +6 -0
- package/src/data/schema/generated/format.ts +5 -0
- package/src/data/schema/generated/type-constructor.ts +10 -0
- package/src/data/schema/generated/type.ts +6 -0
- package/src/data/schema/index.ts +6 -0
- package/src/data/schema/json-schema.ts +33 -0
- package/src/data/schema/namespace.ts +55 -0
- package/src/data/schema/parse.ts +39 -0
- package/src/data/schema/types.ts +39 -0
- package/src/data/temporal/append-only.ts +134 -0
- package/src/data/temporal/close.ts +27 -0
- package/src/data/temporal/columns.ts +10 -0
- package/src/data/temporal/definition-queries.ts +70 -0
- package/src/data/temporal/errors.ts +10 -0
- package/src/data/temporal/get-current.ts +22 -0
- package/src/data/temporal/index.ts +9 -0
- package/src/data/temporal/namespace.ts +36 -0
- package/src/data/temporal/query.ts +25 -0
- package/src/data/temporal/resolve.ts +17 -0
- package/src/data/temporal/revise.ts +67 -0
- package/src/data/temporal/types.ts +22 -0
- package/src/data/valve/artifact.ts +33 -0
- package/src/data/valve/contract-diff.ts +21 -0
- package/src/data/valve/cross.ts +57 -0
- package/src/data/valve/crossing-ledger.ts +50 -0
- package/src/data/valve/decision.ts +44 -0
- package/src/data/valve/durable-crossing.ts +52 -0
- package/src/data/valve/execute-crossing.ts +77 -0
- package/src/data/valve/faucet.ts +56 -0
- package/src/data/valve/gate.ts +114 -0
- package/src/data/valve/graph.ts +77 -0
- package/src/data/valve/index.ts +53 -0
- package/src/data/valve/namespace.ts +57 -0
- package/src/data/valve/registry.ts +50 -0
- package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
- package/src/data/valve/schema/valve-crossing.ts +9 -0
- package/src/data/valve/supply.ts +154 -0
- package/src/data/valve/types.ts +143 -0
- package/src/data/vector/columns.ts +27 -0
- package/src/data/vector/index.ts +4 -0
- package/src/data/vector/namespace.ts +33 -0
- package/src/data/vector/query.ts +85 -0
- package/src/data/vector/validate.ts +16 -0
- package/src/data/write/audit/actor.ts +15 -0
- package/src/data/write/audit/audit-columns.ts +18 -0
- package/src/data/write/audit/audit-select.ts +111 -0
- package/src/data/write/audit/bound-transaction.ts +14 -0
- package/src/data/write/audit/cte-compile.ts +32 -0
- package/src/data/write/audit/decode-returning.ts +47 -0
- package/src/data/write/audit/final-select.ts +43 -0
- package/src/data/write/audit/infra-audit.ts +36 -0
- package/src/data/write/audit/primary-key.ts +65 -0
- package/src/data/write/audit/projection.ts +41 -0
- package/src/data/write/audit/read-touches.ts +86 -0
- package/src/data/write/audit/touch-match.ts +78 -0
- package/src/data/write/audit/verb-delete.ts +62 -0
- package/src/data/write/audit/verb-insert.ts +36 -0
- package/src/data/write/audit/verb-select.ts +47 -0
- package/src/data/write/audit/verb-update.ts +60 -0
- package/src/data/write/audit/verb-upsert.ts +104 -0
- package/src/data/write/conflict.ts +43 -0
- package/src/data/write/delete.ts +38 -0
- package/src/data/write/execute.ts +243 -0
- package/src/data/write/index.ts +11 -0
- package/src/data/write/insert.ts +63 -0
- package/src/data/write/state.ts +43 -0
- package/src/data/write/update.ts +54 -0
- package/src/data/write/upsert.ts +58 -0
- package/src/data/write/values.ts +19 -0
- package/src/index.ts +230 -0
- package/src/infra/access-links/code.ts +70 -0
- package/src/infra/access-links/handle.ts +892 -0
- package/src/infra/access-links/index.ts +9 -0
- package/src/infra/access-links/namespace.ts +30 -0
- package/src/infra/access-links/person-check.ts +177 -0
- package/src/infra/access-links/schema.ts +71 -0
- package/src/infra/alerts/async-sources.ts +231 -0
- package/src/infra/alerts/audit.ts +36 -0
- package/src/infra/alerts/beat.ts +123 -0
- package/src/infra/alerts/client-faults.ts +111 -0
- package/src/infra/alerts/faults.ts +156 -0
- package/src/infra/alerts/handle.ts +778 -0
- package/src/infra/alerts/http.ts +192 -0
- package/src/infra/alerts/index.ts +12 -0
- package/src/infra/alerts/infra-sources.ts +180 -0
- package/src/infra/alerts/namespace.ts +81 -0
- package/src/infra/alerts/redact.ts +21 -0
- package/src/infra/alerts/request-faults.ts +41 -0
- package/src/infra/alerts/schema.ts +124 -0
- package/src/infra/alerts/sinks.ts +125 -0
- package/src/infra/alerts/source-options.ts +26 -0
- package/src/infra/alerts/sources.ts +503 -0
- package/src/infra/alerts/subsystem-sources.ts +137 -0
- package/src/infra/audit/emit.ts +87 -0
- package/src/infra/audit/index.ts +2 -0
- package/src/infra/audit/namespace.ts +22 -0
- package/src/infra/audit/schema.sql +35 -0
- package/src/infra/audit/types.ts +18 -0
- package/src/infra/aws/credentials.ts +147 -0
- package/src/infra/aws/sigv4.ts +140 -0
- package/src/infra/cache/create.ts +58 -0
- package/src/infra/cache/entry.ts +81 -0
- package/src/infra/cache/index.ts +4 -0
- package/src/infra/cache/namespace.ts +10 -0
- package/src/infra/cache/tenant.ts +93 -0
- package/src/infra/cache/types.ts +24 -0
- package/src/infra/crawl/audit.ts +35 -0
- package/src/infra/crawl/errors.ts +25 -0
- package/src/infra/crawl/handle.ts +693 -0
- package/src/infra/crawl/index.ts +9 -0
- package/src/infra/crawl/links.ts +160 -0
- package/src/infra/crawl/namespace.ts +36 -0
- package/src/infra/crawl/robots.ts +100 -0
- package/src/infra/crawl/schema.ts +89 -0
- package/src/infra/credit-line/handle.ts +883 -0
- package/src/infra/credit-line/index.ts +6 -0
- package/src/infra/credit-line/namespace.ts +7 -0
- package/src/infra/credit-line/schema.ts +96 -0
- package/src/infra/credits/handle.ts +706 -0
- package/src/infra/credits/index.ts +5 -0
- package/src/infra/credits/namespace.ts +7 -0
- package/src/infra/credits/schema.ts +83 -0
- package/src/infra/email/audit.ts +36 -0
- package/src/infra/email/inbound.ts +622 -0
- package/src/infra/email/index.ts +11 -0
- package/src/infra/email/mail.ts +517 -0
- package/src/infra/email/mime.ts +447 -0
- package/src/infra/email/namespace.ts +22 -0
- package/src/infra/email/providers.ts +111 -0
- package/src/infra/email/schema.ts +96 -0
- package/src/infra/email/sns.ts +200 -0
- package/src/infra/email/unsubscribe.ts +44 -0
- package/src/infra/fetch/address.ts +88 -0
- package/src/infra/fetch/connect-retry.ts +87 -0
- package/src/infra/fetch/handle.ts +329 -0
- package/src/infra/fetch/index.ts +7 -0
- package/src/infra/fetch/namespace.ts +70 -0
- package/src/infra/idempotency/index.ts +3 -0
- package/src/infra/idempotency/middleware.ts +87 -0
- package/src/infra/idempotency/store.ts +92 -0
- package/src/infra/idempotency/types.ts +30 -0
- package/src/infra/join-requests/handle.ts +312 -0
- package/src/infra/join-requests/index.ts +2 -0
- package/src/infra/join-requests/namespace.ts +7 -0
- package/src/infra/join-requests/schema.ts +33 -0
- package/src/infra/outbox/core.ts +165 -0
- package/src/infra/quota/errors.ts +32 -0
- package/src/infra/quota/handle.ts +215 -0
- package/src/infra/quota/index.ts +5 -0
- package/src/infra/quota/namespace.ts +7 -0
- package/src/infra/quota/schema.ts +15 -0
- package/src/infra/storage/backend.ts +34 -0
- package/src/infra/storage/handle.ts +513 -0
- package/src/infra/storage/index.ts +9 -0
- package/src/infra/storage/local.ts +104 -0
- package/src/infra/storage/namespace.ts +13 -0
- package/src/infra/storage/s3.ts +257 -0
- package/src/infra/storage/schema.ts +24 -0
- package/src/infra/support/handle.ts +854 -0
- package/src/infra/support/http.ts +229 -0
- package/src/infra/support/index.ts +7 -0
- package/src/infra/support/namespace.ts +12 -0
- package/src/infra/support/schema.ts +96 -0
- package/src/infra/webhook/inbound.ts +45 -0
- package/src/infra/webhook/index.ts +5 -0
- package/src/infra/webhook/namespace.ts +24 -0
- package/src/infra/webhook/outbound.ts +315 -0
- package/src/infra/webhook/sign.ts +32 -0
- package/src/infra/webhook/verify.ts +82 -0
- package/src/invariants/assert.ts +26 -0
- package/src/invariants/index.ts +1 -0
- package/src/lint/config.ts +56 -0
- package/src/lint/doctrine.ts +163 -0
- package/src/lint/include.ts +97 -0
- package/src/lint/index.ts +3 -0
- package/src/lint/preset.ts +10 -0
- package/src/lint/resolver.ts +218 -0
- package/src/lint/rules/no-reexports.ts +46 -0
- package/src/request/behavior/audited.ts +85 -0
- package/src/request/behavior/cached.ts +62 -0
- package/src/request/behavior/compose.ts +28 -0
- package/src/request/behavior/index.ts +5 -0
- package/src/request/behavior/namespace.ts +16 -0
- package/src/request/behavior/types.ts +26 -0
- package/src/request/behavior/versioned.ts +54 -0
- package/src/request/context/create.ts +103 -0
- package/src/request/context/guard.ts +49 -0
- package/src/request/context/index.ts +4 -0
- package/src/request/context/run-in-transaction.ts +200 -0
- package/src/request/context/store.ts +28 -0
- package/src/request/context/types.ts +50 -0
- package/src/request/di/builder.ts +535 -0
- package/src/request/di/index.ts +8 -0
- package/src/request/di/resolve.ts +271 -0
- package/src/request/di/types.ts +262 -0
- package/src/request/fitting/namespace.ts +7 -0
- package/src/request/fixture/batch.ts +71 -0
- package/src/request/fixture/builder.ts +26 -0
- package/src/request/fixture/errors.ts +10 -0
- package/src/request/fixture/index.ts +8 -0
- package/src/request/fixture/namespace.ts +23 -0
- package/src/request/fixture/paginate.ts +44 -0
- package/src/request/fixture/to-handlers.ts +260 -0
- package/src/request/fixture/types.ts +79 -0
- package/src/request/flow/act-as.ts +78 -0
- package/src/request/flow/namespace.ts +17 -0
- package/src/request/http/error-step.ts +42 -0
- package/src/request/http/file.ts +141 -0
- package/src/request/http/index.ts +4 -0
- package/src/request/http/middleware.ts +141 -0
- package/src/request/http/namespace.ts +5 -0
- package/src/request/http/rate-limit-classes.ts +170 -0
- package/src/request/http/response.ts +82 -0
- package/src/request/http/server.ts +916 -0
- package/src/request/http/sniff.ts +261 -0
- package/src/request/http/types.ts +250 -0
- package/src/request/http/upload.ts +284 -0
- package/src/request/internal/text.ts +6 -0
- package/src/request/openapi/generate.ts +205 -0
- package/src/request/openapi/index.ts +2 -0
- package/src/request/openapi/namespace.ts +7 -0
- package/src/test/auth.ts +13 -0
- package/src/test/auto-setup.ts +45 -0
- package/src/test/context.ts +91 -0
- package/src/test/crawl-fixture.ts +157 -0
- package/src/test/discover-fork.ts +17 -0
- package/src/test/fingerprint.ts +135 -0
- package/src/test/globals.d.ts +7 -0
- package/src/test/index.ts +19 -0
- package/src/test/namespace.ts +87 -0
- package/src/test/plugin.ts +189 -0
- package/src/test/progress.ts +185 -0
- package/src/test/reap-after-run.ts +28 -0
- package/src/test/reap.ts +286 -0
- package/src/test/reset.ts +253 -0
- package/src/test/savepoints.ts +172 -0
- package/src/test/setup.ts +1422 -0
- package/src/test/system-channel.ts +50 -0
- package/src/test/vitest-plugin.ts +142 -0
- package/src/test/vitest-workspace.ts +507 -0
- package/src/test/vitest.ts +333 -0
- package/src/test/watch.ts +105 -0
- package/src/trace/capture/bounded-serializer.ts +146 -0
- package/src/trace/cardinality/analyze.ts +173 -0
- package/src/trace/cardinality/branch-context.ts +214 -0
- package/src/trace/cardinality/check.ts +33 -0
- package/src/trace/cardinality/classify-edge.ts +64 -0
- package/src/trace/cardinality/structural-max.ts +107 -0
- package/src/trace/check/coverage-check.ts +188 -0
- package/src/trace/check/flow-check.ts +103 -0
- package/src/trace/check/flow-step-site.ts +234 -0
- package/src/trace/check/io-pattern.ts +116 -0
- package/src/trace/codemod/add-cardinality.ts +152 -0
- package/src/trace/codemod/run.ts +80 -0
- package/src/trace/entry/child-trace.ts +21 -0
- package/src/trace/entry/close-trace.ts +87 -0
- package/src/trace/entry/finalize-trace.ts +28 -0
- package/src/trace/entry/open-trace.ts +138 -0
- package/src/trace/entry/parent-trace.ts +20 -0
- package/src/trace/entry/parse-traceparent.ts +39 -0
- package/src/trace/entry/process-start.ts +38 -0
- package/src/trace/entry/trace-context.ts +26 -0
- package/src/trace/entry/tracing-enabled.ts +10 -0
- package/src/trace/external/record.ts +195 -0
- package/src/trace/external/report.ts +66 -0
- package/src/trace/namespace.ts +19 -0
- package/src/trace/partition/day.ts +47 -0
- package/src/trace/partition/index.ts +14 -0
- package/src/trace/partition/partition-name.ts +23 -0
- package/src/trace/partition/plan.ts +82 -0
- package/src/trace/partition/schedule.ts +26 -0
- package/src/trace/partition/startup-hook.ts +39 -0
- package/src/trace/partition/sync.ts +154 -0
- package/src/trace/partition/tables.ts +53 -0
- package/src/trace/read/tree.ts +83 -0
- package/src/trace/retain/copy-on-error.ts +76 -0
- package/src/trace/runtime/expire-stale-traces.ts +54 -0
- package/src/trace/runtime/flow-step.ts +147 -0
- package/src/trace/runtime/flush-steps.ts +31 -0
- package/src/trace/runtime/pending-writes.ts +54 -0
- package/src/trace/runtime/step-buffer.ts +298 -0
- package/src/trace/runtime/step-error.ts +21 -0
- package/src/trace/runtime/step-meta.ts +55 -0
- package/src/trace/runtime/thenable.ts +12 -0
- package/src/trace/runtime/truncate-trace.ts +62 -0
- package/src/trace/schema/trace-fields.ts +52 -0
- package/src/trace/schema/trace-retained.ts +22 -0
- package/src/trace/schema/trace-step-fields.ts +44 -0
- package/src/trace/schema/trace-step-retained.ts +25 -0
- package/src/trace/schema/trace-step.ts +18 -0
- package/src/trace/schema/trace.ts +12 -0
- package/src/trace/transformer/apply.ts +79 -0
- package/src/trace/transformer/build-meta-literal.ts +30 -0
- package/src/trace/transformer/find-flow-step-sites.ts +85 -0
- package/src/trace/transformer/inject-flow-step-import.ts +39 -0
- package/src/trace/transformer/transform-error.ts +14 -0
- package/src/trace/transformer/transform-flow-steps.ts +123 -0
- package/src/trace/transformer/wrap-function-declaration.ts +62 -0
- package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
- package/src/workspace/affected.ts +110 -0
- package/src/workspace/changed-files.ts +163 -0
- package/src/workspace/check-deps.ts +213 -0
- package/src/workspace/checksum.ts +20 -0
- package/src/workspace/dependency-graph.ts +67 -0
- package/src/workspace/detect-pipework-copies.ts +93 -0
- package/src/workspace/discover.ts +206 -0
- package/src/workspace/generate.ts +401 -0
- package/src/workspace/index.ts +7 -0
- package/src/workspace/module-imports-pipework.ts +39 -0
- package/src/workspace/module-package-name.ts +12 -0
- package/src/workspace/scrub-git-env.ts +23 -0
- package/src/workspace/source-paths.ts +36 -0
- package/src/workspace/sync.ts +182 -0
- package/dist/REFERENCE.md +0 -1477
- package/dist/infra/access-links/rate-limit.d.ts +0 -10
- package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
- package/dist/infra/access-links/rate-limit.js +0 -28
- package/dist/infra/access-links/rate-limit.js.map +0 -1
- package/dist/infra/email/sigv4.d.ts +0 -24
- package/dist/infra/email/sigv4.d.ts.map +0 -1
- package/dist/infra/email/sigv4.js +0 -55
- package/dist/infra/email/sigv4.js.map +0 -1
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import { sql } from '../../data/db/sql.js'
|
|
2
|
+
import { pipeSystem } from '../../data/db/db.js'
|
|
3
|
+
import { fitting } from '../../request/di/index.js'
|
|
4
|
+
import { getContext } from '../../request/context/store.js'
|
|
5
|
+
import { getBaseLogger } from '../../core/logging/index.js'
|
|
6
|
+
import type { Queue } from '../../async/jobs/queue.js'
|
|
7
|
+
import type { Handler } from '../../request/di/types.js'
|
|
8
|
+
import { __flowStep } from '../../trace/runtime/flow-step.js'
|
|
9
|
+
import { auditAlert } from './audit.js'
|
|
10
|
+
import { ALERT_SEVERITIES, ALERT_STATUSES } from './schema.js'
|
|
11
|
+
import { encodeCursor, decodeCursor } from '../../request/fixture/paginate.js'
|
|
12
|
+
import { createOutbox } from '../outbox/core.js'
|
|
13
|
+
import type { AlertDelivery, AlertPayload, AlertSink } from './sinks.js'
|
|
14
|
+
import type { AlertSource } from './sources.js'
|
|
15
|
+
import { redactJson, redactText } from './redact.js'
|
|
16
|
+
import { recordBeat, ALERTS_TICK_LOOP, DEFAULT_LOOP_EVERY_MS } from './beat.js'
|
|
17
|
+
import { describeError } from '../../core/logging/error-chain.js'
|
|
18
|
+
import { ValidationError } from '../../core/errors/index.js'
|
|
19
|
+
|
|
20
|
+
export type AlertSeverity = 'info' | 'warning' | 'error' | 'critical'
|
|
21
|
+
|
|
22
|
+
export interface AlertSubject {
|
|
23
|
+
readonly tenantId?: string
|
|
24
|
+
readonly entity?: string
|
|
25
|
+
readonly entityId?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AlertsConfig {
|
|
29
|
+
/** Where alerts are sent. At least one; names must be unique. */
|
|
30
|
+
readonly sinks: readonly AlertSink[]
|
|
31
|
+
/** The queue the alerts job runs on; its worker surface must run `handle.job`. */
|
|
32
|
+
readonly queue: Pick<Queue, 'enqueue'>
|
|
33
|
+
/** Job type of `handle.job`; default `'pipework.alerts'`. */
|
|
34
|
+
readonly jobType?: string
|
|
35
|
+
/** Send again while an alert stays open and is raised again after this long since the last send. Default `null`: never. */
|
|
36
|
+
readonly resendAfterMs?: number | null
|
|
37
|
+
/** Resolve an open alert nobody raised for this long, on the job's tick. Default `null`: only `resolve`. */
|
|
38
|
+
readonly resolveAfterQuietMs?: number | null
|
|
39
|
+
/** Delivery attempts before a row is given up on; default 10. */
|
|
40
|
+
readonly maxAttempts?: number
|
|
41
|
+
/** First retry delay, doubling per attempt up to one hour; default 5000. */
|
|
42
|
+
readonly backoffMs?: number
|
|
43
|
+
/** How long an occurrence is kept; the tick deletes older ones. Default 30 days. */
|
|
44
|
+
readonly occurrenceRetentionMs?: number
|
|
45
|
+
/**
|
|
46
|
+
* Send alerts of these severities in one periodic `digest` delivery per sink instead of one at a time. A raise or
|
|
47
|
+
* resolve of such an alert queues nothing (so `resendAfterMs` has no effect on it); each tick at least `everyMs`
|
|
48
|
+
* after the last digest sends one naming those that changed since (the first, those changed in the last `everyMs`). `severities` default `['warning']`;
|
|
49
|
+
* `error` and `critical` always go one at a time. Default: no digest.
|
|
50
|
+
*/
|
|
51
|
+
readonly digest?: { readonly everyMs: number; readonly severities?: readonly AlertSeverity[] }
|
|
52
|
+
/** The environment label on every delivery; default the raising Flow's `config.environment`. */
|
|
53
|
+
readonly environment?: string
|
|
54
|
+
readonly sources?: readonly AlertSource[]
|
|
55
|
+
/** Database holding the alert tables; default the default database. */
|
|
56
|
+
readonly database?: string
|
|
57
|
+
/** How often you schedule the tick; used to tell when it has stopped. Default 60_000. */
|
|
58
|
+
readonly tickEveryMs?: number
|
|
59
|
+
/**
|
|
60
|
+
* Email addresses in an alert's message and in the strings of its context become `[email]` before the alert is
|
|
61
|
+
* stored, recorded or sent. Each pattern here is replaced by `[redacted]` too (matched globally).
|
|
62
|
+
*/
|
|
63
|
+
readonly redact?: readonly RegExp[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type AlertStatus = 'open' | 'acknowledged' | 'resolved'
|
|
67
|
+
export type JsonValue = string | number | boolean | null | readonly JsonValue[] | { readonly [k: string]: JsonValue }
|
|
68
|
+
|
|
69
|
+
export interface RaiseOptions {
|
|
70
|
+
readonly severity: AlertSeverity
|
|
71
|
+
readonly message: string
|
|
72
|
+
readonly subject?: AlertSubject
|
|
73
|
+
/** Which team most likely fixes it. Latest raise wins. */
|
|
74
|
+
readonly owner?: string
|
|
75
|
+
/** One line: what is safe to do. Latest raise wins. */
|
|
76
|
+
readonly runbook?: string
|
|
77
|
+
/** Ids and counts, stored on the alert (latest raise wins) and on the occurrence. A JSON object. */
|
|
78
|
+
readonly context?: Readonly<Record<string, JsonValue>>
|
|
79
|
+
/** Overrides the handle's `resendAfterMs` for this alert; `null`: this alert is never re-sent. */
|
|
80
|
+
readonly resendAfterMs?: number | null
|
|
81
|
+
/** Overrides the handle's `resolveAfterQuietMs` for this alert; `null`: only `resolve` closes it. */
|
|
82
|
+
readonly resolveAfterQuietMs?: number | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface OwnerChange { readonly from: string | null; readonly to: string; readonly by: string; readonly at: Date }
|
|
86
|
+
|
|
87
|
+
export interface Alert {
|
|
88
|
+
readonly key: string
|
|
89
|
+
readonly status: AlertStatus
|
|
90
|
+
readonly severity: AlertSeverity
|
|
91
|
+
readonly message: string
|
|
92
|
+
readonly subject: AlertSubject | null
|
|
93
|
+
readonly owner: string | null
|
|
94
|
+
/** Each change of owner made by acknowledge, oldest first. Kept across reopens. */
|
|
95
|
+
readonly ownerHistory: readonly OwnerChange[]
|
|
96
|
+
readonly runbook: string | null
|
|
97
|
+
readonly context: Readonly<Record<string, JsonValue>> | null
|
|
98
|
+
/** Raises since first seen, across reopens. */
|
|
99
|
+
readonly count: number
|
|
100
|
+
/** Times a raise came after a resolve. */
|
|
101
|
+
readonly reopenCount: number
|
|
102
|
+
readonly firstSeenAt: Date
|
|
103
|
+
readonly lastSeenAt: Date
|
|
104
|
+
readonly lastSentAt: Date | null
|
|
105
|
+
readonly acknowledgedAt: Date | null
|
|
106
|
+
readonly acknowledgedBy: string | null
|
|
107
|
+
readonly resolvedAt: Date | null
|
|
108
|
+
readonly resolvedBy: string | null
|
|
109
|
+
readonly resolveNote: string | null
|
|
110
|
+
/** The release the resolve names (e.g. the fix's version). */
|
|
111
|
+
readonly resolvedRelease: string | null
|
|
112
|
+
readonly reopenedAt: Date | null
|
|
113
|
+
readonly issueUrl: string | null
|
|
114
|
+
/** Latest ten traces that raised it, newest first. */
|
|
115
|
+
readonly traceIds: readonly string[]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface AlertOccurrence {
|
|
119
|
+
readonly id: string
|
|
120
|
+
readonly key: string
|
|
121
|
+
readonly at: Date
|
|
122
|
+
readonly severity: AlertSeverity
|
|
123
|
+
readonly message: string
|
|
124
|
+
readonly context: Readonly<Record<string, JsonValue>> | null
|
|
125
|
+
readonly traceId: string | null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface Page<T> { readonly items: T[]; readonly next: string | null }
|
|
129
|
+
|
|
130
|
+
export interface AlertTotals {
|
|
131
|
+
/** Every severity, by status; zero counts included for all three statuses and four severities. */
|
|
132
|
+
readonly bySeverity: Record<AlertSeverity, Record<AlertStatus, number>>
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface DeliveryHealth {
|
|
136
|
+
/** Undelivered, not given up. */
|
|
137
|
+
readonly pending: number
|
|
138
|
+
/** Given up (failed_at set). */
|
|
139
|
+
readonly failed: number
|
|
140
|
+
readonly lastDeliveredAt: Date | null
|
|
141
|
+
readonly lastFailedAt: Date | null
|
|
142
|
+
readonly bySink: readonly {
|
|
143
|
+
readonly sink: string; readonly pending: number; readonly failed: number
|
|
144
|
+
readonly lastDeliveredAt: Date | null; readonly lastFailedAt: Date | null; readonly lastError: string | null
|
|
145
|
+
}[]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ResolveOptions {
|
|
149
|
+
readonly note?: string
|
|
150
|
+
readonly release?: string
|
|
151
|
+
readonly by?: string
|
|
152
|
+
/** Queue a `resolve` delivery; default true. */
|
|
153
|
+
readonly notify?: boolean
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface DeliveryRow {
|
|
157
|
+
readonly id: string
|
|
158
|
+
readonly key: string
|
|
159
|
+
readonly kind: AlertPayload['kind']
|
|
160
|
+
readonly sink: string
|
|
161
|
+
readonly payload: AlertPayload
|
|
162
|
+
readonly attempts: number
|
|
163
|
+
readonly nextAttemptAt: Date
|
|
164
|
+
readonly deliveredAt: Date | null
|
|
165
|
+
readonly failedAt: Date | null
|
|
166
|
+
readonly lastError: string | null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface TickResult {
|
|
170
|
+
/** Alerts resolved because nobody raised them for their quiet period, or because a source cleared them. */
|
|
171
|
+
readonly resolved: number
|
|
172
|
+
readonly delivered: number
|
|
173
|
+
/** Attempts that failed and will be retried. */
|
|
174
|
+
readonly retried: number
|
|
175
|
+
/** Deliveries given up on. */
|
|
176
|
+
readonly failed: number
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface Alerts {
|
|
180
|
+
/**
|
|
181
|
+
* Raises the condition `key`. New: opens it and sends once. After a resolve: reopens it (count continues,
|
|
182
|
+
* `reopenCount` + 1) and sends `reopen`. Open: groups it (count, last seen, trace id) and sends nothing, unless
|
|
183
|
+
* `resendAfterMs` has passed since the last send. Acknowledged: groups, never sends. Every raise writes an
|
|
184
|
+
* occurrence. Needs a Flow.
|
|
185
|
+
*/
|
|
186
|
+
raise(key: string, options: RaiseOptions): Promise<{ status: 'opened' | 'grouped' | 'resent' | 'reopened' }>
|
|
187
|
+
/** Resolves an open or acknowledged alert and sends `resolve` once unless `notify` is false. Otherwise: false, nothing sent. */
|
|
188
|
+
resolve(key: string, options?: ResolveOptions): Promise<boolean>
|
|
189
|
+
/**
|
|
190
|
+
* open -> acknowledged. Not open: false. No delivery. `owner` replaces the stored owner and appends to `ownerHistory`
|
|
191
|
+
* (nothing is appended when it is the current owner). An acknowledged alert given a different `owner` changes the owner
|
|
192
|
+
* only, keeps who acknowledged it and when, and returns true; without `owner`, or the same one, it returns false.
|
|
193
|
+
*/
|
|
194
|
+
acknowledge(key: string, options: { by: string; note?: string; owner?: string }): Promise<boolean>
|
|
195
|
+
/** Sets issueUrl on an alert in any status. Unknown key: false. No delivery. Must be an http(s) URL. */
|
|
196
|
+
link(key: string, url: string): Promise<boolean>
|
|
197
|
+
/** Newest `lastSeenAt` first, then key. `limit` default 50, at most 200. */
|
|
198
|
+
list(options?: {
|
|
199
|
+
status?: AlertStatus | readonly AlertStatus[]; severity?: AlertSeverity | readonly AlertSeverity[]
|
|
200
|
+
/** The current owner is one of these. */
|
|
201
|
+
owner?: readonly string[]; limit?: number; cursor?: string
|
|
202
|
+
}): Promise<Page<Alert>>
|
|
203
|
+
/** One alert's raises, newest first. `limit` default 50, at most 200. */
|
|
204
|
+
occurrences(key: string, options?: { limit?: number; cursor?: string }): Promise<Page<AlertOccurrence>>
|
|
205
|
+
totals(): Promise<AlertTotals>
|
|
206
|
+
health(): Promise<DeliveryHealth>
|
|
207
|
+
/** The job to add to a worker surface's `jobs` (and schedule, e.g. every minute): auto-resolve, sources, deliveries. */
|
|
208
|
+
readonly job: Handler<Record<string, unknown>, unknown>
|
|
209
|
+
/** What one run of `job` does; callable from a test. Needs a Flow. */
|
|
210
|
+
tick(): Promise<TickResult>
|
|
211
|
+
open(): Promise<Alert[]>
|
|
212
|
+
get(key: string): Promise<Alert | null>
|
|
213
|
+
deliveries(options?: { key?: string }): Promise<DeliveryRow[]>
|
|
214
|
+
/**
|
|
215
|
+
* Puts the deliveries that were given up (and never delivered) back on the queue, optionally only those of `sink`
|
|
216
|
+
* and/or that failed at or after `failedSince`; the next `tick` sends them. Delivery ids stay the same, so a receiver
|
|
217
|
+
* that dedupes on `delivery.id` takes each once. Writes a `requeue` audit row per delivery naming `by` (who asked:
|
|
218
|
+
* an operator or key name, required).
|
|
219
|
+
*/
|
|
220
|
+
requeueFailed(options: { by: string; sink?: string; failedSince?: Date }): Promise<{ requeued: number }>
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const SEVERITIES = new Set<string>(ALERT_SEVERITIES)
|
|
224
|
+
const STATUSES = new Set<string>(ALERT_STATUSES)
|
|
225
|
+
const DEFAULT_RETENTION_MS = 30 * 24 * 3_600_000
|
|
226
|
+
const RETENTION_BATCH = 10_000
|
|
227
|
+
const DEFAULT_LIMIT = 50
|
|
228
|
+
const MAX_LIMIT = 200
|
|
229
|
+
/** A row's time as text, UTC to the microsecond; a JS Date holds only milliseconds. */
|
|
230
|
+
const exact = (column: string) => sql.raw(`to_char(${column} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`)
|
|
231
|
+
|
|
232
|
+
function toAlert(row: Record<string, unknown>): Alert {
|
|
233
|
+
const date = (v: unknown): Date => new Date(v as string)
|
|
234
|
+
const dateOrNull = (v: unknown): Date | null => (v === null || v === undefined ? null : date(v))
|
|
235
|
+
return {
|
|
236
|
+
key: row['key'] as string,
|
|
237
|
+
status: row['status'] as Alert['status'],
|
|
238
|
+
severity: row['severity'] as AlertSeverity,
|
|
239
|
+
message: row['message'] as string,
|
|
240
|
+
subject: (row['subject'] ?? null) as AlertSubject | null,
|
|
241
|
+
owner: (row['owner'] ?? null) as string | null,
|
|
242
|
+
ownerHistory: ((row['owner_history'] ?? []) as { from: string | null; to: string; by: string; at: string }[])
|
|
243
|
+
.map(h => ({ from: h.from, to: h.to, by: h.by, at: date(h.at) })),
|
|
244
|
+
runbook: (row['runbook'] ?? null) as string | null,
|
|
245
|
+
context: (row['context'] ?? null) as Alert['context'],
|
|
246
|
+
count: Number(row['count']),
|
|
247
|
+
reopenCount: Number(row['reopen_count']),
|
|
248
|
+
firstSeenAt: date(row['first_seen_at']),
|
|
249
|
+
lastSeenAt: date(row['last_seen_at']),
|
|
250
|
+
lastSentAt: dateOrNull(row['last_sent_at']),
|
|
251
|
+
acknowledgedAt: dateOrNull(row['acknowledged_at']),
|
|
252
|
+
acknowledgedBy: (row['acknowledged_by'] ?? null) as string | null,
|
|
253
|
+
resolvedAt: dateOrNull(row['resolved_at']),
|
|
254
|
+
resolvedBy: (row['resolved_by'] ?? null) as string | null,
|
|
255
|
+
resolveNote: (row['resolve_note'] ?? null) as string | null,
|
|
256
|
+
resolvedRelease: (row['resolved_release'] ?? null) as string | null,
|
|
257
|
+
reopenedAt: dateOrNull(row['reopened_at']),
|
|
258
|
+
issueUrl: (row['issue_url'] ?? null) as string | null,
|
|
259
|
+
traceIds: (row['trace_ids'] ?? []) as string[],
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function toDeliveryRow(row: Record<string, unknown>): DeliveryRow {
|
|
264
|
+
return {
|
|
265
|
+
id: row['id'] as string,
|
|
266
|
+
key: row['alert_key'] as string,
|
|
267
|
+
kind: row['kind'] as DeliveryRow['kind'],
|
|
268
|
+
sink: row['sink'] as string,
|
|
269
|
+
payload: row['payload'] as AlertPayload,
|
|
270
|
+
attempts: Number(row['attempts']),
|
|
271
|
+
nextAttemptAt: new Date(row['next_attempt_at'] as string),
|
|
272
|
+
deliveredAt: row['delivered_at'] === null ? null : new Date(row['delivered_at'] as string),
|
|
273
|
+
failedAt: row['failed_at'] === null ? null : new Date(row['failed_at'] as string),
|
|
274
|
+
lastError: (row['last_error'] ?? null) as string | null,
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function toOccurrence(row: Record<string, unknown>): AlertOccurrence {
|
|
279
|
+
return {
|
|
280
|
+
id: row['id'] as string,
|
|
281
|
+
key: row['alert_key'] as string,
|
|
282
|
+
at: new Date(row['at'] as string),
|
|
283
|
+
severity: row['severity'] as AlertSeverity,
|
|
284
|
+
message: row['message'] as string,
|
|
285
|
+
context: (row['context'] ?? null) as AlertOccurrence['context'],
|
|
286
|
+
traceId: (row['trace_id'] ?? null) as string | null,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function checkLimit(fn: string, limit: number | undefined): number {
|
|
291
|
+
if (limit === undefined) return DEFAULT_LIMIT
|
|
292
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
|
|
293
|
+
throw new Error(`[pipework] alerts.${fn}: limit must be a whole number from 1 to ${String(MAX_LIMIT)}, got ${String(limit)}.`)
|
|
294
|
+
}
|
|
295
|
+
return limit
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Reads `[time, tiebreak]` from a cursor this module encoded; anything else is refused by name. */
|
|
299
|
+
function readCursor(fn: string, cursor: string): [string, string] {
|
|
300
|
+
const bad = (): Error => new Error(`[pipework] alerts.${fn}: cursor is not one this handle returned. Pass the \`next\` value unchanged.`)
|
|
301
|
+
let parts: unknown[]
|
|
302
|
+
try { parts = decodeCursor(cursor) } catch { throw bad() }
|
|
303
|
+
const [at, tie] = parts
|
|
304
|
+
if (parts.length !== 2 || typeof at !== 'string' || typeof tie !== 'string' || Number.isNaN(new Date(at).getTime())) throw bad()
|
|
305
|
+
return [at, tie]
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function pickList<T extends string>(fn: string, name: string, value: T | readonly T[] | undefined, allowed: Set<string>): T[] | null {
|
|
309
|
+
if (value === undefined) return null
|
|
310
|
+
const list = typeof value === 'string' ? [value] : [...value]
|
|
311
|
+
for (const v of list) {
|
|
312
|
+
if (!allowed.has(v)) throw new Error(`[pipework] alerts.${fn}: ${name} must be one of ${[...allowed].join(', ')}, got "${String(v)}".`)
|
|
313
|
+
}
|
|
314
|
+
return list
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function pgTextArray(values: readonly string[]): string {
|
|
318
|
+
return `{${values.map(v => `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`).join(',')}}`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function createAlerts(config: AlertsConfig): Alerts {
|
|
322
|
+
if (config.sinks.length === 0) throw new Error('[pipework] alerts.create needs at least one sink.')
|
|
323
|
+
const sinks = new Map<string, AlertSink>()
|
|
324
|
+
for (const s of config.sinks) {
|
|
325
|
+
if (sinks.has(s.name)) throw new Error(`[pipework] alerts.create: two sinks are named "${s.name}"; give each its own name.`)
|
|
326
|
+
sinks.set(s.name, s)
|
|
327
|
+
}
|
|
328
|
+
const sinkNames = pgTextArray([...sinks.keys()])
|
|
329
|
+
const digestEveryMs = config.digest?.everyMs ?? null
|
|
330
|
+
if (config.digest !== undefined) {
|
|
331
|
+
if (!Number.isInteger(config.digest.everyMs) || config.digest.everyMs < 1000) {
|
|
332
|
+
throw new Error(`[pipework] alerts.create: digest.everyMs must be a whole number of at least 1000, got ${String(config.digest.everyMs)}.`)
|
|
333
|
+
}
|
|
334
|
+
for (const v of config.digest.severities ?? []) {
|
|
335
|
+
if (v === 'error' || v === 'critical') throw new Error(`[pipework] alerts.create: digest.severities cannot include "${v}"; errors and critical go one at a time.`)
|
|
336
|
+
if (!SEVERITIES.has(v)) throw new Error(`[pipework] alerts.create: digest.severities must be from info, warning, got "${String(v)}".`)
|
|
337
|
+
}
|
|
338
|
+
if (config.digest.severities?.length === 0) throw new Error('[pipework] alerts.create: digest.severities needs at least one severity, or leave it out for warning.')
|
|
339
|
+
}
|
|
340
|
+
const digestSeverities = pgTextArray(config.digest === undefined ? [] : [...new Set(config.digest.severities ?? ['warning'])])
|
|
341
|
+
const jobType = config.jobType ?? 'pipework.alerts'
|
|
342
|
+
const maxAttempts = config.maxAttempts ?? 10
|
|
343
|
+
const backoffMs = config.backoffMs ?? 5000
|
|
344
|
+
const defaultResend = config.resendAfterMs ?? null
|
|
345
|
+
const defaultQuiet = config.resolveAfterQuietMs ?? null
|
|
346
|
+
const sources = config.sources ?? []
|
|
347
|
+
const database = config.database
|
|
348
|
+
const redactExtra = config.redact ?? []
|
|
349
|
+
const log = getBaseLogger()
|
|
350
|
+
|
|
351
|
+
const db = () => pipeSystem(database)
|
|
352
|
+
|
|
353
|
+
const retentionMs = config.occurrenceRetentionMs ?? DEFAULT_RETENTION_MS
|
|
354
|
+
const environmentOf = (): string => config.environment ?? getContext()?.instance.config.environment ?? 'unknown'
|
|
355
|
+
|
|
356
|
+
// `null` is stored as -1 ("never"); undefined as NULL ("the handle's default").
|
|
357
|
+
function storedMs(name: string, value: number | null | undefined): number | null {
|
|
358
|
+
if (value === undefined) return null
|
|
359
|
+
if (value === null) return -1
|
|
360
|
+
if (!Number.isInteger(value) || value < 0) throw new Error(`[pipework] alerts.raise: ${name} must be a non-negative integer or null, got ${String(value)}.`)
|
|
361
|
+
return value
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// The alert and its deliveries move in one statement, so a crash cannot leave one without the other.
|
|
365
|
+
const payloadSql = (kind: ReturnType<typeof sql>, environment: string) => sql`jsonb_build_object(
|
|
366
|
+
'kind', ${kind}, 'key', up.key, 'severity', up.severity, 'message', up.message, 'subject', up.subject,
|
|
367
|
+
'owner', up.owner, 'runbook', up.runbook, 'context', up.context,
|
|
368
|
+
'count', up.count, 'reopenCount', up.reopen_count, 'firstSeenAt', up.first_seen_at, 'lastSeenAt', up.last_seen_at,
|
|
369
|
+
'resolvedAt', up.resolved_at, 'acknowledgedAt', up.acknowledged_at, 'acknowledgedBy', up.acknowledged_by,
|
|
370
|
+
'resolvedBy', up.resolved_by, 'resolveNote', up.resolve_note, 'resolvedRelease', up.resolved_release,
|
|
371
|
+
'issueUrl', up.issue_url, 'environment', ${environment}::text, 'traceIds', to_jsonb(up.trace_ids))`
|
|
372
|
+
|
|
373
|
+
async function enqueueJob(): Promise<void> {
|
|
374
|
+
await config.queue.enqueue(db(), { type: jobType, payload: {}, continueFlow: true })
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Inside a tick the deliveries are sent by that same tick, so it does not enqueue another.
|
|
378
|
+
async function raise(key: string, options: RaiseOptions, enqueue = true): Promise<{ status: 'opened' | 'grouped' | 'resent' | 'reopened' }> {
|
|
379
|
+
if (key.trim() === '') throw new Error('[pipework] alerts.raise needs a non-empty key.')
|
|
380
|
+
if (!SEVERITIES.has(options.severity)) throw new Error(`[pipework] alerts.raise: severity must be info, warning, error or critical, got "${String(options.severity)}".`)
|
|
381
|
+
const resendStored = storedMs('resendAfterMs', options.resendAfterMs)
|
|
382
|
+
const quietStored = storedMs('resolveAfterQuietMs', options.resolveAfterQuietMs)
|
|
383
|
+
const traceId = getContext()?.trace?.id ?? null
|
|
384
|
+
const traceArray = traceId === null ? '{}' : pgTextArray([traceId])
|
|
385
|
+
const subject = options.subject === undefined ? null : JSON.stringify(options.subject)
|
|
386
|
+
const context = options.context === undefined ? null : JSON.stringify(redactJson(options.context, redactExtra))
|
|
387
|
+
const message = redactText(options.message, redactExtra)
|
|
388
|
+
const environment = environmentOf()
|
|
389
|
+
// `last_seen_at` never moves back. A raise sends exactly when this statement sets `last_sent_at`, and then sets
|
|
390
|
+
// `last_seen_at` to the same value; a raise that does not send never leaves them equal, so RETURNING can tell the two apart.
|
|
391
|
+
const seenSql = sql`GREATEST(a.last_seen_at, EXCLUDED.last_seen_at)`
|
|
392
|
+
const sendsSql = sql`(a.status <> 'acknowledged' AND (a.status = 'resolved' OR a.last_sent_at IS NULL
|
|
393
|
+
OR (NULLIF(COALESCE(EXCLUDED.resend_after_ms, a.resend_after_ms, ${defaultResend}::int), -1) IS NOT NULL
|
|
394
|
+
AND EXCLUDED.last_sent_at - a.last_sent_at
|
|
395
|
+
>= NULLIF(COALESCE(EXCLUDED.resend_after_ms, a.resend_after_ms, ${defaultResend}::int), -1) * interval '1 millisecond')))`
|
|
396
|
+
const kindSql = sql`CASE WHEN up.inserted THEN 'raise' WHEN up.reopened THEN 'reopen' ELSE 'repeat' END`
|
|
397
|
+
const rows = await db().execute(sql`
|
|
398
|
+
WITH up AS (
|
|
399
|
+
INSERT INTO pipework_alerts AS a
|
|
400
|
+
(key, status, severity, message, subject, owner, runbook, context, count, first_seen_at, last_seen_at, last_sent_at,
|
|
401
|
+
trace_ids, resend_after_ms, resolve_after_quiet_ms, updated_at)
|
|
402
|
+
SELECT ${key}, 'open', ${options.severity}, ${message}, ${subject}::jsonb,
|
|
403
|
+
${options.owner ?? null}::text, ${options.runbook ?? null}::text, ${context}::jsonb, 1, c.ts, c.ts, c.ts,
|
|
404
|
+
${traceArray}::text[], ${resendStored}::int, ${quietStored}::int, c.ts
|
|
405
|
+
FROM (SELECT clock_timestamp() AS ts) c
|
|
406
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
407
|
+
status = CASE WHEN a.status = 'acknowledged' THEN 'acknowledged' ELSE 'open' END,
|
|
408
|
+
severity = EXCLUDED.severity,
|
|
409
|
+
message = EXCLUDED.message,
|
|
410
|
+
subject = EXCLUDED.subject,
|
|
411
|
+
owner = CASE WHEN jsonb_array_length(a.owner_history) > 0 THEN a.owner ELSE EXCLUDED.owner END,
|
|
412
|
+
runbook = EXCLUDED.runbook,
|
|
413
|
+
context = EXCLUDED.context,
|
|
414
|
+
count = a.count + 1,
|
|
415
|
+
reopen_count = CASE WHEN a.status = 'resolved' THEN a.reopen_count + 1 ELSE a.reopen_count END,
|
|
416
|
+
reopened_at = CASE WHEN a.status = 'resolved' THEN ${seenSql} ELSE a.reopened_at END,
|
|
417
|
+
acknowledged_at = CASE WHEN a.status = 'resolved' THEN NULL ELSE a.acknowledged_at END,
|
|
418
|
+
acknowledged_by = CASE WHEN a.status = 'resolved' THEN NULL ELSE a.acknowledged_by END,
|
|
419
|
+
last_seen_at = CASE WHEN ${sendsSql} OR ${seenSql} IS DISTINCT FROM a.last_sent_at THEN ${seenSql}
|
|
420
|
+
ELSE ${seenSql} + interval '1 microsecond' END,
|
|
421
|
+
resolved_at = NULL,
|
|
422
|
+
trace_ids = CASE
|
|
423
|
+
WHEN a.status = 'resolved' THEN EXCLUDED.trace_ids
|
|
424
|
+
WHEN ${traceId}::text IS NULL THEN a.trace_ids
|
|
425
|
+
ELSE (EXCLUDED.trace_ids || array_remove(a.trace_ids, ${traceId}::text))[1:10] END,
|
|
426
|
+
resend_after_ms = COALESCE(EXCLUDED.resend_after_ms, CASE WHEN a.status = 'resolved' THEN NULL ELSE a.resend_after_ms END),
|
|
427
|
+
resolve_after_quiet_ms = COALESCE(EXCLUDED.resolve_after_quiet_ms, CASE WHEN a.status = 'resolved' THEN NULL ELSE a.resolve_after_quiet_ms END),
|
|
428
|
+
last_sent_at = CASE WHEN ${sendsSql} THEN ${seenSql} ELSE a.last_sent_at END,
|
|
429
|
+
updated_at = EXCLUDED.updated_at
|
|
430
|
+
RETURNING a.*, (a.last_sent_at = a.last_seen_at) AS sent, (xmax = 0) AS inserted,
|
|
431
|
+
(a.reopened_at IS NOT NULL AND a.reopened_at = a.last_seen_at) AS reopened
|
|
432
|
+
),
|
|
433
|
+
occ AS (
|
|
434
|
+
INSERT INTO pipework_alert_occurrences (alert_key, at, severity, message, context, trace_id)
|
|
435
|
+
SELECT up.key, up.last_seen_at, up.severity, up.message, up.context, ${traceId}::text FROM up
|
|
436
|
+
),
|
|
437
|
+
dels AS (
|
|
438
|
+
INSERT INTO pipework_alert_deliveries (alert_key, kind, sink, payload, created_at, next_attempt_at)
|
|
439
|
+
SELECT up.key, ${kindSql}, s.sink,
|
|
440
|
+
${payloadSql(kindSql, environment)}, clock_timestamp(), clock_timestamp()
|
|
441
|
+
FROM up CROSS JOIN unnest(${sinkNames}::text[]) AS s(sink)
|
|
442
|
+
WHERE up.sent AND NOT up.severity = ANY(${digestSeverities}::text[])
|
|
443
|
+
RETURNING id
|
|
444
|
+
)
|
|
445
|
+
SELECT up.*, (SELECT count(*) FROM dels)::int AS queued FROM up`)
|
|
446
|
+
const row = rows[0]!
|
|
447
|
+
const alert = toAlert(row)
|
|
448
|
+
const sent = row['sent'] === true
|
|
449
|
+
const status = !sent ? 'grouped' : row['inserted'] === true ? 'opened' : row['reopened'] === true ? 'reopened' : 'resent'
|
|
450
|
+
await auditAlert(status === 'opened' ? 'raise' : status === 'reopened' ? 'reopen' : 'group', key, {
|
|
451
|
+
status, severity: alert.severity, count: alert.count, message: alert.message,
|
|
452
|
+
}, database)
|
|
453
|
+
if (sent && enqueue && Number(row['queued']) > 0) await enqueueJob()
|
|
454
|
+
return { status }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function resolveKey(key: string, onlyIfQuiet: boolean, enqueue = true, options: ResolveOptions = {}): Promise<boolean> {
|
|
458
|
+
const notify = options.notify ?? true
|
|
459
|
+
const environment = environmentOf()
|
|
460
|
+
const rows = await db().execute(sql`
|
|
461
|
+
WITH up AS (
|
|
462
|
+
UPDATE pipework_alerts SET status = 'resolved', resolved_at = clock_timestamp(), updated_at = clock_timestamp(),
|
|
463
|
+
resolved_by = ${onlyIfQuiet ? sql`'quiet'` : sql`${options.by ?? null}::text`},
|
|
464
|
+
resolve_note = ${onlyIfQuiet ? sql`'quiet for ' || COALESCE(resolve_after_quiet_ms, ${defaultQuiet}::int) || ' ms'` : sql`${options.note ?? null}::text`},
|
|
465
|
+
resolved_release = ${options.release ?? null}::text
|
|
466
|
+
WHERE key = ${key} AND status IN ('open', 'acknowledged')
|
|
467
|
+
AND (NOT ${onlyIfQuiet} OR (resolve_after_quiet_ms IS DISTINCT FROM -1 AND last_seen_at < clock_timestamp()
|
|
468
|
+
- COALESCE(resolve_after_quiet_ms, ${defaultQuiet}::int) * interval '1 millisecond'))
|
|
469
|
+
RETURNING *
|
|
470
|
+
),
|
|
471
|
+
dels AS (
|
|
472
|
+
INSERT INTO pipework_alert_deliveries (alert_key, kind, sink, payload, created_at, next_attempt_at)
|
|
473
|
+
SELECT up.key, 'resolve', s.sink, ${payloadSql(sql`'resolve'`, environment)}, clock_timestamp(), clock_timestamp()
|
|
474
|
+
FROM up CROSS JOIN unnest(${sinkNames}::text[]) AS s(sink)
|
|
475
|
+
WHERE ${notify}::boolean AND NOT up.severity = ANY(${digestSeverities}::text[])
|
|
476
|
+
RETURNING id
|
|
477
|
+
)
|
|
478
|
+
SELECT up.*, (SELECT count(*) FROM dels)::int AS queued FROM up`)
|
|
479
|
+
if (rows.length === 0) return false
|
|
480
|
+
const alert = toAlert(rows[0]!)
|
|
481
|
+
await auditAlert('resolve', key, { count: alert.count, auto: onlyIfQuiet, ...(onlyIfQuiet ? { by: 'quiet' } : options.by !== undefined ? { by: options.by } : {}) }, database)
|
|
482
|
+
if (enqueue && notify && Number(rows[0]!['queued']) > 0) await enqueueJob()
|
|
483
|
+
return true
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function acknowledge(key: string, options: { by: string; note?: string; owner?: string }): Promise<boolean> {
|
|
487
|
+
if (options.by.trim() === '') throw new Error('[pipework] alerts.acknowledge needs a non-empty by.')
|
|
488
|
+
if (options.owner !== undefined && options.owner.trim() === '') throw new Error('[pipework] alerts.acknowledge needs a non-empty owner when one is given.')
|
|
489
|
+
const owner = options.owner?.trim() ?? null
|
|
490
|
+
const rows = await db().execute(sql`
|
|
491
|
+
UPDATE pipework_alerts SET
|
|
492
|
+
status = 'acknowledged',
|
|
493
|
+
acknowledged_at = CASE WHEN status = 'open' THEN clock_timestamp() ELSE acknowledged_at END,
|
|
494
|
+
acknowledged_by = CASE WHEN status = 'open' THEN ${options.by} ELSE acknowledged_by END,
|
|
495
|
+
owner = COALESCE(${owner}::text, owner),
|
|
496
|
+
owner_history = CASE WHEN ${owner}::text IS NOT NULL AND ${owner}::text IS DISTINCT FROM owner
|
|
497
|
+
THEN owner_history || jsonb_build_array(jsonb_build_object('from', owner, 'to', ${owner}::text, 'by', ${options.by}::text,
|
|
498
|
+
'at', to_char(clock_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')))
|
|
499
|
+
ELSE owner_history END,
|
|
500
|
+
updated_at = clock_timestamp()
|
|
501
|
+
WHERE key = ${key}
|
|
502
|
+
AND (status = 'open' OR (status = 'acknowledged' AND ${owner}::text IS NOT NULL AND ${owner}::text IS DISTINCT FROM owner))
|
|
503
|
+
RETURNING key`)
|
|
504
|
+
if (rows.length === 0) return false
|
|
505
|
+
await auditAlert('acknowledge', key, {
|
|
506
|
+
by: options.by, ...(options.note !== undefined ? { note: options.note } : {}), ...(owner !== null ? { owner } : {}),
|
|
507
|
+
}, database)
|
|
508
|
+
return true
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function link(key: string, url: string): Promise<boolean> {
|
|
512
|
+
let parsed: URL
|
|
513
|
+
try { parsed = new URL(url) } catch { throw new Error(`[pipework] alerts.link: url must be an http or https URL, got "${url}".`) }
|
|
514
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
515
|
+
throw new Error(`[pipework] alerts.link: url must be an http or https URL, got "${url}".`)
|
|
516
|
+
}
|
|
517
|
+
const rows = await db().execute(sql`UPDATE pipework_alerts SET issue_url = ${url}, updated_at = clock_timestamp() WHERE key = ${key} RETURNING key`)
|
|
518
|
+
if (rows.length === 0) return false
|
|
519
|
+
await auditAlert('link', key, { url }, database)
|
|
520
|
+
return true
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function list(options: Parameters<Alerts['list']>[0] = {}): Promise<Page<Alert>> {
|
|
524
|
+
const statuses = pickList('list', 'status', options.status, STATUSES)
|
|
525
|
+
const severities = pickList('list', 'severity', options.severity, SEVERITIES)
|
|
526
|
+
const owners = options.owner ?? null
|
|
527
|
+
const limit = checkLimit('list', options.limit)
|
|
528
|
+
const cursor = options.cursor === undefined ? null : readCursor('list', options.cursor)
|
|
529
|
+
const rows = await db().execute(sql`
|
|
530
|
+
SELECT *, ${exact('last_seen_at')} AS exact_at FROM pipework_alerts
|
|
531
|
+
WHERE (${statuses === null}::boolean OR status = ANY(${pgTextArray(statuses ?? [])}::text[]))
|
|
532
|
+
AND (${severities === null}::boolean OR severity = ANY(${pgTextArray(severities ?? [])}::text[]))
|
|
533
|
+
AND (${owners === null}::boolean OR owner = ANY(${pgTextArray([...(owners ?? [])])}::text[]))
|
|
534
|
+
AND (${cursor === null}::boolean OR last_seen_at < ${cursor?.[0] ?? null}::timestamptz
|
|
535
|
+
OR (last_seen_at = ${cursor?.[0] ?? null}::timestamptz AND key > ${cursor?.[1] ?? null}::text))
|
|
536
|
+
ORDER BY last_seen_at DESC, key ASC
|
|
537
|
+
LIMIT ${limit + 1}`)
|
|
538
|
+
const page = rows.slice(0, limit)
|
|
539
|
+
const last = page[page.length - 1]
|
|
540
|
+
return {
|
|
541
|
+
items: page.map(toAlert),
|
|
542
|
+
next: rows.length > limit && last !== undefined ? encodeCursor([last['exact_at'], last['key']]) : null,
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function occurrences(key: string, options: { limit?: number; cursor?: string } = {}): Promise<Page<AlertOccurrence>> {
|
|
547
|
+
const limit = checkLimit('occurrences', options.limit)
|
|
548
|
+
const cursor = options.cursor === undefined ? null : readCursor('occurrences', options.cursor)
|
|
549
|
+
const rows = await db().execute(sql`
|
|
550
|
+
SELECT *, ${exact('at')} AS exact_at FROM pipework_alert_occurrences
|
|
551
|
+
WHERE alert_key = ${key}
|
|
552
|
+
AND (${cursor === null}::boolean OR at < ${cursor?.[0] ?? null}::timestamptz
|
|
553
|
+
OR (at = ${cursor?.[0] ?? null}::timestamptz AND id < ${cursor?.[1] ?? null}::uuid))
|
|
554
|
+
ORDER BY at DESC, id DESC
|
|
555
|
+
LIMIT ${limit + 1}`)
|
|
556
|
+
const page = rows.slice(0, limit)
|
|
557
|
+
const last = page[page.length - 1]
|
|
558
|
+
return {
|
|
559
|
+
items: page.map(toOccurrence),
|
|
560
|
+
next: rows.length > limit && last !== undefined ? encodeCursor([last['exact_at'], last['id']]) : null,
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function totals(): Promise<AlertTotals> {
|
|
565
|
+
const rows = await db().execute(sql`SELECT severity, status, count(*)::int AS n FROM pipework_alerts GROUP BY severity, status`)
|
|
566
|
+
const bySeverity = {} as Record<AlertSeverity, Record<AlertStatus, number>>
|
|
567
|
+
for (const sev of ALERT_SEVERITIES) bySeverity[sev] = { open: 0, acknowledged: 0, resolved: 0 }
|
|
568
|
+
for (const row of rows) bySeverity[row['severity'] as AlertSeverity][row['status'] as AlertStatus] = Number(row['n'])
|
|
569
|
+
return { bySeverity }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function health(): Promise<DeliveryHealth> {
|
|
573
|
+
const rows = await db().execute(sql`
|
|
574
|
+
SELECT sink,
|
|
575
|
+
count(*) FILTER (WHERE delivered_at IS NULL AND failed_at IS NULL)::int AS pending,
|
|
576
|
+
count(*) FILTER (WHERE failed_at IS NOT NULL)::int AS failed,
|
|
577
|
+
max(delivered_at) AS last_delivered_at,
|
|
578
|
+
max(failed_at) AS last_failed_at,
|
|
579
|
+
(array_agg(last_error ORDER BY COALESCE(failed_at, next_attempt_at) DESC) FILTER (WHERE last_error IS NOT NULL))[1] AS last_error
|
|
580
|
+
FROM pipework_alert_deliveries GROUP BY sink`)
|
|
581
|
+
const toDate = (v: unknown): Date | null => (v === null || v === undefined ? null : new Date(v as string))
|
|
582
|
+
const bySink = new Map<string, DeliveryHealth['bySink'][number]>()
|
|
583
|
+
for (const name of sinks.keys()) {
|
|
584
|
+
bySink.set(name, { sink: name, pending: 0, failed: 0, lastDeliveredAt: null, lastFailedAt: null, lastError: null })
|
|
585
|
+
}
|
|
586
|
+
let pending = 0
|
|
587
|
+
let failed = 0
|
|
588
|
+
let lastDeliveredAt: Date | null = null
|
|
589
|
+
let lastFailedAt: Date | null = null
|
|
590
|
+
const later = (a: Date | null, b: Date | null): Date | null => (a === null ? b : b === null ? a : a > b ? a : b)
|
|
591
|
+
for (const row of rows) {
|
|
592
|
+
const entry = {
|
|
593
|
+
sink: row['sink'] as string, pending: Number(row['pending']), failed: Number(row['failed']),
|
|
594
|
+
lastDeliveredAt: toDate(row['last_delivered_at']), lastFailedAt: toDate(row['last_failed_at']),
|
|
595
|
+
lastError: (row['last_error'] ?? null) as string | null,
|
|
596
|
+
}
|
|
597
|
+
bySink.set(entry.sink, entry)
|
|
598
|
+
pending += entry.pending
|
|
599
|
+
failed += entry.failed
|
|
600
|
+
lastDeliveredAt = later(lastDeliveredAt, entry.lastDeliveredAt)
|
|
601
|
+
lastFailedAt = later(lastFailedAt, entry.lastFailedAt)
|
|
602
|
+
}
|
|
603
|
+
return { pending, failed, lastDeliveredAt, lastFailedAt, bySink: [...bySink.values()].sort((x, y) => x.sink.localeCompare(y.sink)) }
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function pruneOccurrences(): Promise<void> {
|
|
607
|
+
await db().execute(sql`
|
|
608
|
+
DELETE FROM pipework_alert_occurrences WHERE id IN (
|
|
609
|
+
SELECT id FROM pipework_alert_occurrences
|
|
610
|
+
WHERE at < clock_timestamp() - ${retentionMs}::bigint * interval '1 millisecond' LIMIT ${RETENTION_BATCH})`)
|
|
611
|
+
await db().execute(sql`
|
|
612
|
+
DELETE FROM pipework_faults WHERE id IN (
|
|
613
|
+
SELECT id FROM pipework_faults
|
|
614
|
+
WHERE recorded_at < clock_timestamp() - ${retentionMs}::bigint * interval '1 millisecond' LIMIT ${RETENTION_BATCH})`)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function autoResolve(): Promise<number> {
|
|
618
|
+
if (defaultQuiet === null) {
|
|
619
|
+
const any = await db().execute(sql`SELECT 1 FROM pipework_alerts WHERE status IN ('open', 'acknowledged') AND resolve_after_quiet_ms IS NOT NULL AND resolve_after_quiet_ms <> -1 LIMIT 1`)
|
|
620
|
+
if (any.length === 0) return 0
|
|
621
|
+
}
|
|
622
|
+
const due = await db().execute(sql`
|
|
623
|
+
SELECT key FROM pipework_alerts
|
|
624
|
+
WHERE status IN ('open', 'acknowledged')
|
|
625
|
+
AND resolve_after_quiet_ms IS DISTINCT FROM -1
|
|
626
|
+
AND last_seen_at < clock_timestamp() - COALESCE(resolve_after_quiet_ms, ${defaultQuiet}::int) * interval '1 millisecond'`)
|
|
627
|
+
let resolved = 0
|
|
628
|
+
for (const row of due) if (await resolveKey(row['key'] as string, true, false)) resolved++
|
|
629
|
+
return resolved
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function runSources(): Promise<number> {
|
|
633
|
+
let resolved = 0
|
|
634
|
+
for (const source of sources) {
|
|
635
|
+
let conditions
|
|
636
|
+
try {
|
|
637
|
+
conditions = await source.evaluate()
|
|
638
|
+
} catch (err) {
|
|
639
|
+
log.error({ err, source: source.name }, 'alert source failed')
|
|
640
|
+
try {
|
|
641
|
+
await raise(`alert-source-failed:${source.name}`, {
|
|
642
|
+
severity: 'error',
|
|
643
|
+
message: describeError(err).message,
|
|
644
|
+
}, false)
|
|
645
|
+
} catch (raiseErr) {
|
|
646
|
+
log.error({ err: raiseErr, source: source.name }, 'alert source failure could not be raised')
|
|
647
|
+
}
|
|
648
|
+
continue
|
|
649
|
+
}
|
|
650
|
+
if (await resolveKey(`alert-source-failed:${source.name}`, false, false)) resolved++
|
|
651
|
+
for (const c of conditions) {
|
|
652
|
+
await raise(c.key, {
|
|
653
|
+
severity: c.severity,
|
|
654
|
+
message: c.message,
|
|
655
|
+
...(c.subject !== undefined ? { subject: c.subject } : {}),
|
|
656
|
+
...(c.owner !== undefined ? { owner: c.owner } : {}),
|
|
657
|
+
...(c.runbook !== undefined ? { runbook: c.runbook } : {}),
|
|
658
|
+
...(c.context !== undefined ? { context: c.context } : {}),
|
|
659
|
+
}, false)
|
|
660
|
+
}
|
|
661
|
+
if (source.owns === undefined) continue
|
|
662
|
+
const live = new Set(conditions.map(c => c.key))
|
|
663
|
+
const opened = await db().execute(sql`SELECT key FROM pipework_alerts WHERE status IN ('open', 'acknowledged')`)
|
|
664
|
+
for (const row of opened) {
|
|
665
|
+
const key = row['key'] as string
|
|
666
|
+
if (source.owns(key) && !live.has(key) && await resolveKey(key, false, false)) resolved++
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return resolved
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const DIGEST_KEY = 'pipework.alerts.digest'
|
|
673
|
+
|
|
674
|
+
// One statement claims the window (only the handle that moves the cursor wins) and queues the digest.
|
|
675
|
+
async function sendDigest(): Promise<void> {
|
|
676
|
+
if (digestEveryMs === null) return
|
|
677
|
+
const environment = environmentOf()
|
|
678
|
+
const rows = await db().execute(sql`
|
|
679
|
+
WITH old AS (SELECT cursor_at FROM pipework_alert_source_state WHERE name = ${DIGEST_KEY}),
|
|
680
|
+
claim AS (
|
|
681
|
+
INSERT INTO pipework_alert_source_state AS s (name, cursor_at) VALUES (${DIGEST_KEY}, statement_timestamp())
|
|
682
|
+
ON CONFLICT (name) DO UPDATE SET cursor_at = EXCLUDED.cursor_at
|
|
683
|
+
WHERE s.cursor_at <= clock_timestamp() - ${digestEveryMs}::bigint * interval '1 millisecond'
|
|
684
|
+
RETURNING cursor_at AS until
|
|
685
|
+
),
|
|
686
|
+
changed AS (
|
|
687
|
+
SELECT a.* FROM pipework_alerts a, claim
|
|
688
|
+
WHERE a.severity = ANY(${digestSeverities}::text[])
|
|
689
|
+
AND a.updated_at > COALESCE((SELECT cursor_at FROM old), statement_timestamp() - ${digestEveryMs}::bigint * interval '1 millisecond')
|
|
690
|
+
),
|
|
691
|
+
dels AS (
|
|
692
|
+
INSERT INTO pipework_alert_deliveries (alert_key, kind, sink, payload, created_at, next_attempt_at)
|
|
693
|
+
SELECT ${DIGEST_KEY}, 'digest', s.sink,
|
|
694
|
+
jsonb_build_object('kind', 'digest', 'key', ${DIGEST_KEY}::text, 'environment', ${environment}::text,
|
|
695
|
+
'since', (SELECT cursor_at FROM old), 'until', claim.until,
|
|
696
|
+
'alerts', (SELECT jsonb_agg(jsonb_build_object('key', c.key, 'severity', c.severity, 'status', c.status,
|
|
697
|
+
'message', c.message, 'owner', c.owner, 'count', c.count, 'reopenCount', c.reopen_count,
|
|
698
|
+
'lastSeenAt', c.last_seen_at) ORDER BY c.key) FROM changed c)),
|
|
699
|
+
clock_timestamp(), clock_timestamp()
|
|
700
|
+
FROM claim CROSS JOIN unnest(${sinkNames}::text[]) AS s(sink)
|
|
701
|
+
WHERE EXISTS (SELECT 1 FROM changed)
|
|
702
|
+
RETURNING id
|
|
703
|
+
)
|
|
704
|
+
SELECT (SELECT count(*) FROM claim)::int AS claimed, (SELECT count(*) FROM dels)::int AS queued,
|
|
705
|
+
(SELECT coalesce(array_agg(key ORDER BY key), '{}') FROM changed) AS keys`)
|
|
706
|
+
const row = rows[0]!
|
|
707
|
+
if (Number(row['queued']) > 0) await auditAlert('digest', DIGEST_KEY, { keys: row['keys'] }, database)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const outbox = createOutbox<AlertDelivery>({
|
|
711
|
+
table: 'pipework_alert_deliveries', groupColumn: 'alert_key', database, sinks, maxAttempts, backoffMs,
|
|
712
|
+
label: 'alert delivery given up',
|
|
713
|
+
toDelivery: (row, attempt) => ({
|
|
714
|
+
id: row['id'] as string, kind: row['kind'] as AlertDelivery['kind'], key: row['alert_key'] as string,
|
|
715
|
+
sink: row['sink'] as string, payload: row['payload'] as AlertPayload, attempt,
|
|
716
|
+
}),
|
|
717
|
+
onDelivered: (row, attempt) => auditAlert('deliver', row['alert_key'] as string, { sink: row['sink'], kind: row['kind'], attempt }, database),
|
|
718
|
+
onGivenUp: (row, attempt, error) => auditAlert('deliver-failed', row['alert_key'] as string, { sink: row['sink'], kind: row['kind'], attempt, error }, database),
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
async function runTick(): Promise<TickResult> {
|
|
722
|
+
let resolved = await autoResolve()
|
|
723
|
+
resolved += await runSources()
|
|
724
|
+
await pruneOccurrences()
|
|
725
|
+
await sendDigest()
|
|
726
|
+
return { resolved, ...await outbox.drain() }
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const flowTick = __flowStep({ ref: 'pipework/alerts:tick' }, runTick)
|
|
730
|
+
const tickEveryMs = config.tickEveryMs ?? DEFAULT_LOOP_EVERY_MS
|
|
731
|
+
const tickStep = async (): Promise<TickResult> => {
|
|
732
|
+
try {
|
|
733
|
+
const result = await flowTick()
|
|
734
|
+
await recordBeat(db, ALERTS_TICK_LOOP, { ok: true, everyMs: tickEveryMs })
|
|
735
|
+
return result
|
|
736
|
+
} catch (err) {
|
|
737
|
+
await recordBeat(db, ALERTS_TICK_LOOP, { ok: false, error: describeError(err).message, everyMs: tickEveryMs })
|
|
738
|
+
throw err
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const job = (database !== undefined ? fitting.use(database) : fitting.use()).job(jobType).fit(async () => tickStep())
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
raise: (key, options) => raise(key, options),
|
|
745
|
+
resolve: (key, options) => resolveKey(key, false, true, options),
|
|
746
|
+
acknowledge,
|
|
747
|
+
link,
|
|
748
|
+
list,
|
|
749
|
+
occurrences,
|
|
750
|
+
totals,
|
|
751
|
+
health,
|
|
752
|
+
job: job as unknown as Handler<Record<string, unknown>, unknown>,
|
|
753
|
+
tick: () => tickStep(),
|
|
754
|
+
async requeueFailed(options) {
|
|
755
|
+
if (typeof options.by !== 'string' || options.by.trim() === '') throw new ValidationError({ by: ['must be a non-empty string'] }, 'alerts.requeueFailed needs by, who is requeueing.')
|
|
756
|
+
const by = options.by.trim()
|
|
757
|
+
const requeued = await outbox.requeue({ ...(options.sink !== undefined ? { sink: options.sink } : {}), ...(options.failedSince !== undefined ? { failedSince: options.failedSince } : {}) })
|
|
758
|
+
for (const row of requeued) {
|
|
759
|
+
await auditAlert('requeue', row['alert_key'] as string, { sink: row['sink'], kind: row['kind'], by, attempts: Number(row['prev_attempts']), error: row['last_error'] ?? null }, database)
|
|
760
|
+
}
|
|
761
|
+
return { requeued: requeued.length }
|
|
762
|
+
},
|
|
763
|
+
async open() {
|
|
764
|
+
const rows = await db().execute(sql`SELECT * FROM pipework_alerts WHERE status = 'open' ORDER BY first_seen_at, key`)
|
|
765
|
+
return rows.map(toAlert)
|
|
766
|
+
},
|
|
767
|
+
async get(key) {
|
|
768
|
+
const rows = await db().execute(sql`SELECT * FROM pipework_alerts WHERE key = ${key}`)
|
|
769
|
+
return rows[0] === undefined ? null : toAlert(rows[0])
|
|
770
|
+
},
|
|
771
|
+
async deliveries(options = {}) {
|
|
772
|
+
const rows = options.key === undefined
|
|
773
|
+
? await db().execute(sql`SELECT * FROM pipework_alert_deliveries ORDER BY created_at, id`)
|
|
774
|
+
: await db().execute(sql`SELECT * FROM pipework_alert_deliveries WHERE alert_key = ${options.key} ORDER BY created_at, id`)
|
|
775
|
+
return rows.map(toDeliveryRow)
|
|
776
|
+
},
|
|
777
|
+
}
|
|
778
|
+
}
|