pipework 0.252.0 → 0.253.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/CHANGELOG.md +10 -0
- package/dist/infra/support/handle.d.ts +8 -1
- package/dist/infra/support/handle.d.ts.map +1 -1
- package/dist/infra/support/handle.js +9 -2
- package/dist/infra/support/handle.js.map +1 -1
- package/dist/infra/support/namespace.d.ts.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 +869 -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,125 @@
|
|
|
1
|
+
import { signPayload } from '../webhook/sign.js'
|
|
2
|
+
import type { WebhookSignatureOptions } from '../webhook/sign.js'
|
|
3
|
+
import type { Sink, SinkDelivery } from '../outbox/core.js'
|
|
4
|
+
|
|
5
|
+
/** One alert's event, delivered at once. */
|
|
6
|
+
export interface AlertEventPayload {
|
|
7
|
+
readonly kind: 'raise' | 'repeat' | 'resolve' | 'reopen'
|
|
8
|
+
readonly key: string
|
|
9
|
+
readonly severity: 'info' | 'warning' | 'error' | 'critical'
|
|
10
|
+
readonly message: string
|
|
11
|
+
readonly subject: { readonly tenantId?: string; readonly entity?: string; readonly entityId?: string } | null
|
|
12
|
+
readonly owner: string | null
|
|
13
|
+
readonly runbook: string | null
|
|
14
|
+
readonly context: Readonly<Record<string, unknown>> | null
|
|
15
|
+
readonly count: number
|
|
16
|
+
/** Times a raise came after a resolve. */
|
|
17
|
+
readonly reopenCount: number
|
|
18
|
+
readonly firstSeenAt: string
|
|
19
|
+
readonly lastSeenAt: string
|
|
20
|
+
readonly resolvedAt: string | null
|
|
21
|
+
readonly acknowledgedAt: string | null
|
|
22
|
+
readonly acknowledgedBy: string | null
|
|
23
|
+
/** On a `reopen`, those of the resolve it reopens. */
|
|
24
|
+
readonly resolvedBy: string | null
|
|
25
|
+
readonly resolveNote: string | null
|
|
26
|
+
readonly resolvedRelease: string | null
|
|
27
|
+
readonly issueUrl: string | null
|
|
28
|
+
readonly environment: string
|
|
29
|
+
/** The latest traces that raised it, newest first. */
|
|
30
|
+
readonly traceIds: readonly string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The alerts of the digest severities that changed in one window, sent once per sink. */
|
|
34
|
+
export interface AlertDigestPayload {
|
|
35
|
+
readonly kind: 'digest'
|
|
36
|
+
readonly key: 'pipework.alerts.digest'
|
|
37
|
+
readonly environment: string
|
|
38
|
+
/** The previous digest's time; null on the first. */
|
|
39
|
+
readonly since: string | null
|
|
40
|
+
readonly until: string
|
|
41
|
+
readonly alerts: readonly {
|
|
42
|
+
readonly key: string
|
|
43
|
+
readonly severity: 'info' | 'warning' | 'error' | 'critical'
|
|
44
|
+
readonly status: 'open' | 'acknowledged' | 'resolved'
|
|
45
|
+
readonly message: string
|
|
46
|
+
readonly owner: string | null
|
|
47
|
+
readonly count: number
|
|
48
|
+
readonly reopenCount: number
|
|
49
|
+
readonly lastSeenAt: string
|
|
50
|
+
}[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What a sink receives, and what the webhook sink POSTs as its body; tell them apart by `kind`. */
|
|
54
|
+
export type AlertPayload = AlertEventPayload | AlertDigestPayload
|
|
55
|
+
|
|
56
|
+
export type AlertDelivery = SinkDelivery<AlertPayload>
|
|
57
|
+
|
|
58
|
+
/** A place alerts are sent; see `Sink`. */
|
|
59
|
+
export type AlertSink = Sink<AlertPayload>
|
|
60
|
+
|
|
61
|
+
export interface AlertWebhookConfig {
|
|
62
|
+
/** Sink name; default `'webhook'`. Two sinks on one handle need distinct names. */
|
|
63
|
+
readonly name?: string
|
|
64
|
+
readonly url: string
|
|
65
|
+
readonly secret: string
|
|
66
|
+
/** Request timeout; default 10 000, at most 20 000 (the handle gives a send 10 s and leases the row for 30 s). */
|
|
67
|
+
readonly timeoutMs?: number
|
|
68
|
+
readonly signing?: WebhookSignatureOptions
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* POSTs the payload as JSON with `X-Webhook-Id`, `X-Webhook-Timestamp` and `X-Webhook-Signature`
|
|
73
|
+
* (`X-Webhook-Id` is the delivery id, the same on every attempt; HMAC over `<id>.<timestamp>.<body>`, as `webhook.sign` makes them). Any non-2xx status fails the attempt.
|
|
74
|
+
*/
|
|
75
|
+
export function webhookSink<P extends { readonly kind: string } = AlertPayload>(config: AlertWebhookConfig): Sink<P> {
|
|
76
|
+
const timeoutMs = config.timeoutMs ?? 10_000
|
|
77
|
+
if (timeoutMs > 20_000) {
|
|
78
|
+
throw new Error(`[pipework] alerts.webhook: timeoutMs ${String(timeoutMs)} is over 20000. A delivery is leased for 30 s while it is sent; a longer send could be picked up and sent twice.`)
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name: config.name ?? 'webhook',
|
|
82
|
+
async send(delivery) {
|
|
83
|
+
const signed = signPayload(delivery.payload, config.secret, { ...config.signing, messageId: delivery.id })
|
|
84
|
+
const controller = new AbortController()
|
|
85
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch(config.url, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
'X-Webhook-Id': signed.messageId,
|
|
92
|
+
'X-Webhook-Timestamp': signed.timestamp.toString(),
|
|
93
|
+
'X-Webhook-Signature': signed.signature,
|
|
94
|
+
},
|
|
95
|
+
body: signed.body,
|
|
96
|
+
signal: controller.signal,
|
|
97
|
+
})
|
|
98
|
+
await response.text().catch(() => '')
|
|
99
|
+
if (response.status < 200 || response.status >= 300) throw new Error(`webhook answered HTTP ${String(response.status)}`)
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer)
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface MemoryAlertSink<P extends { readonly kind: string } = AlertPayload> extends Sink<P> {
|
|
108
|
+
/** Every delivery received, in order. */
|
|
109
|
+
readonly delivered: SinkDelivery<P>[]
|
|
110
|
+
clear(): void
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A sink that keeps what it receives, for tests. Generic over the payload so it fits `support.create` (`memorySink<SupportEventPayload>()`, or inferred where a support sink is expected). */
|
|
114
|
+
export function memorySink<P extends { readonly kind: string } = AlertPayload>(options?: { name?: string }): MemoryAlertSink<P>
|
|
115
|
+
export function memorySink(options?: { name?: string }): MemoryAlertSink
|
|
116
|
+
export function memorySink(options: { name?: string } = {}): MemoryAlertSink<{ readonly kind: string }> {
|
|
117
|
+
const delivered: SinkDelivery<{ readonly kind: string }>[] = []
|
|
118
|
+
return {
|
|
119
|
+
name: options.name ?? 'memory',
|
|
120
|
+
delivered,
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- the sink contract is async
|
|
122
|
+
async send(delivery) { delivered.push(delivery) },
|
|
123
|
+
clear() { delivered.length = 0 },
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Options and helpers shared by the alert sources built on `querySource` and `newRowsSource`.
|
|
2
|
+
|
|
3
|
+
import type { AlertSeverity } from './handle.js'
|
|
4
|
+
|
|
5
|
+
export interface Basics { owner?: string; runbook?: string; severity?: AlertSeverity; database?: string }
|
|
6
|
+
|
|
7
|
+
export const FIFTEEN_MIN = 15 * 60_000
|
|
8
|
+
export const HOUR = 3_600_000
|
|
9
|
+
|
|
10
|
+
export function positive(value: number, label: string, who: string): number {
|
|
11
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
12
|
+
throw new Error(`[pipework] ${who} ${label} must be a positive number of milliseconds, got ${String(value)}.`)
|
|
13
|
+
}
|
|
14
|
+
return Math.trunc(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function common(options: Basics): Basics {
|
|
18
|
+
return {
|
|
19
|
+
...(options.owner !== undefined ? { owner: options.owner } : {}),
|
|
20
|
+
...(options.runbook !== undefined ? { runbook: options.runbook } : {}),
|
|
21
|
+
...(options.severity !== undefined ? { severity: options.severity } : {}),
|
|
22
|
+
...(options.database !== undefined ? { database: options.database } : {}),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const text = (v: unknown): string => v === null || v === undefined ? 'none' : String(v)
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { sql } from '../../data/db/sql.js'
|
|
3
|
+
import { pipeSystem } from '../../data/db/db.js'
|
|
4
|
+
import { assertValidIdentifier, quoteIdentifier } from '../../data/db/identifiers.js'
|
|
5
|
+
import type { HealthCheck } from '../../core/lifecycle/types.js'
|
|
6
|
+
import type { SQL } from '../../data/db/index.js'
|
|
7
|
+
import type { Manifold } from '../../core/pipework.js'
|
|
8
|
+
import type { AlertSeverity, JsonValue } from './handle.js'
|
|
9
|
+
import { ALERTS_TICK_LOOP, LOOP_BEATS_QUERY, staleLoops } from './beat.js'
|
|
10
|
+
import { getContext } from '../../request/context/store.js'
|
|
11
|
+
import { describeError } from '../../core/logging/error-chain.js'
|
|
12
|
+
|
|
13
|
+
/** A condition a source finds true right now. */
|
|
14
|
+
export interface AlertCondition {
|
|
15
|
+
readonly key: string
|
|
16
|
+
readonly severity: AlertSeverity
|
|
17
|
+
readonly message: string
|
|
18
|
+
readonly subject?: { tenantId?: string; entity?: string; entityId?: string }
|
|
19
|
+
/** Passed to `alerts.raise` with the condition: who answers it, where the runbook is, and structured detail. */
|
|
20
|
+
readonly owner?: string
|
|
21
|
+
readonly runbook?: string
|
|
22
|
+
readonly context?: Readonly<Record<string, JsonValue>>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Evaluated on every tick. Each returned condition is raised (so an ongoing one groups). When
|
|
27
|
+
* `owns` is given, an open alert the source owns that it no longer returns is resolved.
|
|
28
|
+
*/
|
|
29
|
+
export interface AlertSource {
|
|
30
|
+
readonly name: string
|
|
31
|
+
owns?(key: string): boolean
|
|
32
|
+
evaluate(): Promise<AlertCondition[]>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface TableOption { readonly table?: string; readonly database?: string }
|
|
36
|
+
|
|
37
|
+
type Row = Record<string, unknown>
|
|
38
|
+
|
|
39
|
+
interface SourceBasics { readonly severity?: AlertSeverity; readonly owner?: string; readonly runbook?: string; readonly database?: string }
|
|
40
|
+
|
|
41
|
+
function assertSourceName(name: string, who: string): void {
|
|
42
|
+
if (typeof name !== 'string' || name === '' || name.includes(':')) {
|
|
43
|
+
throw new Error(`[pipework] ${who}: name must be a non-empty string without ':', got ${JSON.stringify(name)}.`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ownerFields(options: { owner?: string; runbook?: string }): { owner?: string; runbook?: string } {
|
|
48
|
+
return {
|
|
49
|
+
...(options.owner !== undefined ? { owner: options.owner } : {}),
|
|
50
|
+
...(options.runbook !== undefined ? { runbook: options.runbook } : {}),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One source from several: it evaluates each in turn and owns what any of them owns. */
|
|
55
|
+
export function composeSources(name: string, parts: readonly AlertSource[]): AlertSource {
|
|
56
|
+
const owning = parts.filter(p => p.owns !== undefined)
|
|
57
|
+
return {
|
|
58
|
+
name,
|
|
59
|
+
...(owning.length > 0 ? { owns: (key: string) => owning.some(p => p.owns!(key)) } : {}),
|
|
60
|
+
async evaluate() {
|
|
61
|
+
const out: AlertCondition[] = []
|
|
62
|
+
for (const part of parts) out.push(...await part.evaluate())
|
|
63
|
+
return out
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface QuerySourceOptions extends SourceBasics {
|
|
69
|
+
/** Key prefix: conditions are keyed `${name}:${id}`. Non-empty, no ':'. */
|
|
70
|
+
readonly name: string
|
|
71
|
+
/** Returns one row per subject that is in trouble right now. */
|
|
72
|
+
readonly query: SQL
|
|
73
|
+
readonly id: (row: Row) => string
|
|
74
|
+
readonly message: (row: Row) => string
|
|
75
|
+
readonly context?: (row: Row) => Record<string, JsonValue>
|
|
76
|
+
readonly subject?: (row: Row) => AlertCondition['subject']
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* For a condition that is true right now: runs `query` (as the system, any tenant) on every tick and raises
|
|
81
|
+
* `${name}:${id}` for each row. An open alert under `${name}:` that a tick no longer returns is resolved.
|
|
82
|
+
*/
|
|
83
|
+
export function querySource(options: QuerySourceOptions): AlertSource {
|
|
84
|
+
assertSourceName(options.name, 'alerts.source.query')
|
|
85
|
+
const prefix = `${options.name}:`
|
|
86
|
+
const severity = options.severity ?? 'error'
|
|
87
|
+
return {
|
|
88
|
+
name: options.name,
|
|
89
|
+
owns: key => key.startsWith(prefix),
|
|
90
|
+
async evaluate() {
|
|
91
|
+
const rows = await pipeSystem(options.database).execute(options.query)
|
|
92
|
+
return rows.map((r: Row): AlertCondition => {
|
|
93
|
+
const subject = options.subject?.(r)
|
|
94
|
+
const context = options.context?.(r)
|
|
95
|
+
return {
|
|
96
|
+
key: `${prefix}${options.id(r)}`,
|
|
97
|
+
severity,
|
|
98
|
+
message: options.message(r),
|
|
99
|
+
...(subject !== undefined ? { subject } : {}),
|
|
100
|
+
...ownerFields(options),
|
|
101
|
+
...(context !== undefined ? { context } : {}),
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface NewRowsGroup { readonly id: string; readonly count: number; readonly last: Row; readonly sum: number }
|
|
109
|
+
|
|
110
|
+
export interface NewRowsSourceOptions extends SourceBasics {
|
|
111
|
+
/** Key prefix: conditions are keyed `${name}:${id}`; also the cursor's state key unless `stateKey` is given. */
|
|
112
|
+
readonly name: string
|
|
113
|
+
/** The table holding the recorded failures (an identifier, not schema-qualified). */
|
|
114
|
+
readonly table: string
|
|
115
|
+
/** The timestamp column a row is stamped with when the failure is recorded. */
|
|
116
|
+
readonly at: string
|
|
117
|
+
/** Extra filter; the table is aliased `t`. */
|
|
118
|
+
readonly where?: SQL
|
|
119
|
+
/** A column of the table, or an SQL expression over alias `t`; one condition per distinct value. */
|
|
120
|
+
readonly groupBy: string | SQL
|
|
121
|
+
readonly message: (group: NewRowsGroup) => string
|
|
122
|
+
readonly context?: (group: NewRowsGroup) => Record<string, JsonValue>
|
|
123
|
+
readonly subject?: (group: NewRowsGroup) => AlertCondition['subject']
|
|
124
|
+
/** A numeric column to total per group, handed to the callbacks as `sum` (0 without it). */
|
|
125
|
+
readonly sum?: string
|
|
126
|
+
/** Rows are read once they are older than this many ms (default 30_000); see `newRowsSource`. */
|
|
127
|
+
readonly settleMs?: number
|
|
128
|
+
/** The cursor's key in `pipework_alert_source_state`; default `name`. */
|
|
129
|
+
readonly stateKey?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* For a condition that is "new since the last tick": raises `${name}:${id}` for each group of rows in `table`
|
|
134
|
+
* stamped in `at` after the cursor. The cursor lives in `pipework_alert_source_state`, shared by every process;
|
|
135
|
+
* the first evaluation only stores it, so history is not replayed. Each evaluation reads rows with
|
|
136
|
+
* `cursor < at <= clock_timestamp() - settleMs` and moves the cursor to that upper bound, so a row stamped with
|
|
137
|
+
* its transaction's start time that commits within `settleMs` is still read. `settleMs` must exceed the longest
|
|
138
|
+
* transaction that writes the table. It has no `owns`: the condition clears by the handle's quiet period
|
|
139
|
+
* (`resolveAfterQuietMs` on `alerts.create`).
|
|
140
|
+
*/
|
|
141
|
+
export function newRowsSource(options: NewRowsSourceOptions): AlertSource {
|
|
142
|
+
assertSourceName(options.name, 'alerts.source.newRows')
|
|
143
|
+
const settleMs = options.settleMs ?? 30_000
|
|
144
|
+
if (typeof settleMs !== 'number' || !Number.isFinite(settleMs) || settleMs < 0) {
|
|
145
|
+
throw new Error(`[pipework] alerts.source.newRows settleMs must be a non-negative number of milliseconds, got ${String(settleMs)}.`)
|
|
146
|
+
}
|
|
147
|
+
const table = quoteIdentifier(options.table, 'newRows table')
|
|
148
|
+
const at = `t.${quoteIdentifier(options.at, 'newRows at column')}`
|
|
149
|
+
const group = typeof options.groupBy === 'string' ? sql.raw(`t.${quoteIdentifier(options.groupBy, 'newRows groupBy column')}`) : options.groupBy
|
|
150
|
+
const total = options.sum === undefined ? sql`0` : sql.raw(`sum(t.${quoteIdentifier(options.sum, 'newRows sum column')})`)
|
|
151
|
+
const where = options.where === undefined ? sql`` : sql`AND (${options.where})`
|
|
152
|
+
const stateKey = options.stateKey ?? options.name
|
|
153
|
+
const severity = options.severity ?? 'error'
|
|
154
|
+
const prefix = `${options.name}:`
|
|
155
|
+
return {
|
|
156
|
+
name: options.name,
|
|
157
|
+
async evaluate() {
|
|
158
|
+
const db = pipeSystem(options.database)
|
|
159
|
+
const first = await db.execute(sql`
|
|
160
|
+
INSERT INTO pipework_alert_source_state (name, cursor_at)
|
|
161
|
+
VALUES (${stateKey}, clock_timestamp() - ${settleMs} * interval '1 millisecond')
|
|
162
|
+
ON CONFLICT (name) DO NOTHING RETURNING name`)
|
|
163
|
+
if (first.length > 0) return []
|
|
164
|
+
const rows = await db.execute(sql`
|
|
165
|
+
WITH cur AS (
|
|
166
|
+
SELECT cursor_at, clock_timestamp() - ${settleMs} * interval '1 millisecond' AS upto
|
|
167
|
+
FROM pipework_alert_source_state WHERE name = ${stateKey} FOR UPDATE),
|
|
168
|
+
totals AS (
|
|
169
|
+
SELECT (${group})::text AS gid, count(*)::int AS n, (${total})::float8 AS total
|
|
170
|
+
FROM ${sql.raw(table)} t, cur
|
|
171
|
+
WHERE ${sql.raw(at)} > cur.cursor_at AND ${sql.raw(at)} <= cur.upto ${where}
|
|
172
|
+
GROUP BY 1),
|
|
173
|
+
newest AS (
|
|
174
|
+
SELECT DISTINCT ON (1) (${group})::text AS gid, to_jsonb(t) AS last
|
|
175
|
+
FROM ${sql.raw(table)} t, cur
|
|
176
|
+
WHERE ${sql.raw(at)} > cur.cursor_at AND ${sql.raw(at)} <= cur.upto ${where}
|
|
177
|
+
ORDER BY 1, ${sql.raw(at)} DESC),
|
|
178
|
+
found AS (
|
|
179
|
+
SELECT totals.gid AS id, n, total, last FROM totals JOIN newest ON newest.gid IS NOT DISTINCT FROM totals.gid),
|
|
180
|
+
moved AS (
|
|
181
|
+
UPDATE pipework_alert_source_state s SET cursor_at = GREATEST(s.cursor_at, cur.upto)
|
|
182
|
+
FROM cur WHERE s.name = ${stateKey} RETURNING 1)
|
|
183
|
+
SELECT id, n, total, last, (SELECT count(*) FROM moved) AS moved FROM found ORDER BY id`)
|
|
184
|
+
return rows.map((r: Row): AlertCondition => {
|
|
185
|
+
const last = (typeof r['last'] === 'string' ? JSON.parse(r['last']) : r['last']) as Row
|
|
186
|
+
const g: NewRowsGroup = { id: r['id'] === null ? '(none)' : String(r['id']), count: Number(r['n']), last, sum: Number(r['total']) }
|
|
187
|
+
const subject = options.subject?.(g)
|
|
188
|
+
const context = options.context?.(g)
|
|
189
|
+
return {
|
|
190
|
+
key: `${prefix}${g.id}`,
|
|
191
|
+
severity,
|
|
192
|
+
message: options.message(g),
|
|
193
|
+
...(subject !== undefined ? { subject } : {}),
|
|
194
|
+
...ownerFields(options),
|
|
195
|
+
...(context !== undefined ? { context } : {}),
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function jobsTable(table: string | undefined): string {
|
|
203
|
+
const name = table ?? 'pipework_jobs'
|
|
204
|
+
assertValidIdentifier(name, 'queue table')
|
|
205
|
+
return name
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Raises `job-dead-letter:<jobType>` for jobs that exhausted their attempts since the last
|
|
210
|
+
* evaluation (any process: the position is kept in `pipework_alert_source_state`; the first
|
|
211
|
+
* evaluation starts from now, so history is not replayed). The condition clears only by the
|
|
212
|
+
* handle's quiet period, so set `resolveAfterQuietMs` on `alerts.create`. `types` limits it to those job
|
|
213
|
+
* types and `excludeTypes` leaves them out (not both, neither empty); each table and each filter keeps its own position.
|
|
214
|
+
* Jobs are read `settleMs` (default 30_000) after they dead-letter, so one that commits late is not missed.
|
|
215
|
+
* Conditions carry `owner`, `runbook` and a context of `count` and `lastError`.
|
|
216
|
+
*/
|
|
217
|
+
export function deadLetterSource(options: TableOption & { severity?: AlertSeverity; types?: readonly string[]; excludeTypes?: readonly string[]; owner?: string; runbook?: string; settleMs?: number } = {}): AlertSource {
|
|
218
|
+
if (options.types !== undefined && options.excludeTypes !== undefined) {
|
|
219
|
+
throw new Error('[pipework] alerts.source.deadLetter takes types or excludeTypes, not both.')
|
|
220
|
+
}
|
|
221
|
+
for (const opt of ['types', 'excludeTypes'] as const) {
|
|
222
|
+
if (options[opt] !== undefined && options[opt].length === 0) {
|
|
223
|
+
throw new Error(`[pipework] alerts.source.deadLetter ${opt} must not be empty; omit it to include every job type.`)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const jobs = jobsTable(options.table)
|
|
227
|
+
const name = 'job-dead-letter'
|
|
228
|
+
const mode = options.types !== undefined ? 'only' : options.excludeTypes !== undefined ? 'except' : null
|
|
229
|
+
const listed = [...new Set(options.types ?? options.excludeTypes ?? [])].sort()
|
|
230
|
+
const joined = listed.join(',')
|
|
231
|
+
const base = jobs === 'pipework_jobs' ? name : `${name}@${jobs}`
|
|
232
|
+
const stateKey = mode === null ? base
|
|
233
|
+
: `${base}:${mode}:${joined.length > 200 ? createHash('sha256').update(joined).digest('hex') : joined}`
|
|
234
|
+
const listJson = JSON.stringify(listed)
|
|
235
|
+
const typeFilter = mode === 'only' ? sql`AND t.job_type IN (SELECT jsonb_array_elements_text(${listJson}::jsonb))`
|
|
236
|
+
: mode === 'except' ? sql`AND t.job_type NOT IN (SELECT jsonb_array_elements_text(${listJson}::jsonb))`
|
|
237
|
+
: sql``
|
|
238
|
+
const lastError = (g: NewRowsGroup): string | null => {
|
|
239
|
+
const payload = g.last['error_payload']
|
|
240
|
+
const message = typeof payload === 'object' && payload !== null ? (payload as Row)['message'] : undefined
|
|
241
|
+
return message === null || message === undefined ? null : String(message)
|
|
242
|
+
}
|
|
243
|
+
return newRowsSource({
|
|
244
|
+
name,
|
|
245
|
+
stateKey,
|
|
246
|
+
table: jobs,
|
|
247
|
+
at: 'completed_at',
|
|
248
|
+
where: sql`t.status = 'dead_letter' ${typeFilter}`,
|
|
249
|
+
groupBy: 'job_type',
|
|
250
|
+
...(options.settleMs !== undefined ? { settleMs: options.settleMs } : {}),
|
|
251
|
+
...(options.severity !== undefined ? { severity: options.severity } : {}),
|
|
252
|
+
...(options.database !== undefined ? { database: options.database } : {}),
|
|
253
|
+
...ownerFields(options),
|
|
254
|
+
message: g => `${String(g.count)} ${g.id} job(s) dead-lettered` + (lastError(g) !== null ? `; last error: ${lastError(g)!}` : ''),
|
|
255
|
+
subject: g => ({ entity: 'job', entityId: g.id }),
|
|
256
|
+
context: g => ({ count: g.count, lastError: lastError(g) }),
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Raises `job-long-running:<jobType>` while a job of that type has been claimed for longer than `olderThanMs`; resolves when none is. */
|
|
261
|
+
export function longRunningSource(options: TableOption & { olderThanMs: number; severity?: AlertSeverity }): AlertSource {
|
|
262
|
+
const jobs = quoteIdentifier(jobsTable(options.table), 'queue table')
|
|
263
|
+
return querySource({
|
|
264
|
+
name: 'job-long-running',
|
|
265
|
+
severity: options.severity ?? 'warning',
|
|
266
|
+
...(options.database !== undefined ? { database: options.database } : {}),
|
|
267
|
+
query: sql`
|
|
268
|
+
SELECT job_type, count(*)::int AS n, (extract(epoch FROM clock_timestamp() - min(claimed_at)) * 1000)::bigint AS oldest_ms
|
|
269
|
+
FROM ${sql.raw(jobs)}
|
|
270
|
+
WHERE status IN ('claimed', 'running')
|
|
271
|
+
AND claimed_at < clock_timestamp() - ${options.olderThanMs} * interval '1 millisecond'
|
|
272
|
+
GROUP BY job_type ORDER BY job_type`,
|
|
273
|
+
id: r => String(r['job_type']),
|
|
274
|
+
message: r => `${String(r['n'])} ${String(r['job_type'])} job(s) running for more than ${String(options.olderThanMs)} ms (oldest ${String(r['oldest_ms'])} ms)`,
|
|
275
|
+
subject: r => ({ entity: 'job', entityId: String(r['job_type']) }),
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Raises `queue-backlog:<jobType>` while more than `maxPending` jobs of a type are ready to claim, or the
|
|
281
|
+
* oldest has waited more than `maxOldestAgeMs`; resolves when the type is under both. Each condition carries
|
|
282
|
+
* `owner` and `runbook` and a context of `ready`, `running` and `oldestReadyWaitMs`.
|
|
283
|
+
*/
|
|
284
|
+
export function backlogSource(options: TableOption & { maxPending?: number; maxOldestAgeMs?: number; severity?: AlertSeverity; owner?: string; runbook?: string }): AlertSource {
|
|
285
|
+
if (options.maxPending === undefined && options.maxOldestAgeMs === undefined) {
|
|
286
|
+
throw new Error('[pipework] alerts.source.backlog needs maxPending, maxOldestAgeMs or both.')
|
|
287
|
+
}
|
|
288
|
+
const jobs = quoteIdentifier(jobsTable(options.table), 'queue table')
|
|
289
|
+
const maxPending = options.maxPending ?? null
|
|
290
|
+
const maxAge = options.maxOldestAgeMs ?? null
|
|
291
|
+
return querySource({
|
|
292
|
+
name: 'queue-backlog',
|
|
293
|
+
severity: options.severity ?? 'warning',
|
|
294
|
+
...(options.database !== undefined ? { database: options.database } : {}),
|
|
295
|
+
...ownerFields(options),
|
|
296
|
+
query: sql`
|
|
297
|
+
SELECT job_type, n, running, oldest_ms FROM (
|
|
298
|
+
SELECT job_type,
|
|
299
|
+
(count(*) FILTER (WHERE status = 'pending' AND (next_retry_at IS NULL OR next_retry_at <= clock_timestamp())))::int AS n,
|
|
300
|
+
(count(*) FILTER (WHERE status IN ('claimed', 'running')))::int AS running,
|
|
301
|
+
(extract(epoch FROM clock_timestamp() - min(queued_at) FILTER (WHERE status = 'pending' AND (next_retry_at IS NULL OR next_retry_at <= clock_timestamp()))) * 1000)::bigint AS oldest_ms
|
|
302
|
+
FROM ${sql.raw(jobs)}
|
|
303
|
+
WHERE status IN ('pending', 'claimed', 'running')
|
|
304
|
+
GROUP BY job_type) p
|
|
305
|
+
WHERE (${maxPending}::int IS NOT NULL AND n > ${maxPending}::int)
|
|
306
|
+
OR (${maxAge}::bigint IS NOT NULL AND oldest_ms > ${maxAge}::bigint)
|
|
307
|
+
ORDER BY job_type`,
|
|
308
|
+
id: r => String(r['job_type']),
|
|
309
|
+
message: r => `${String(r['n'])} ${String(r['job_type'])} job(s) waiting (${String(r['running'])} running), the oldest for ${String(Number(r['oldest_ms']))} ms`,
|
|
310
|
+
subject: r => ({ entity: 'job', entityId: String(r['job_type']) }),
|
|
311
|
+
context: r => ({ ready: Number(r['n']), running: Number(r['running']), oldestReadyWaitMs: Number(r['oldest_ms']) }),
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Raises `health:<name>` while a check reports unhealthy or throws; resolves when it is healthy again. Degraded is not a fault. */
|
|
316
|
+
export function healthSource(checks: readonly HealthCheck[], options: { severity?: AlertSeverity } = {}): AlertSource {
|
|
317
|
+
const severity = options.severity ?? 'error'
|
|
318
|
+
const keys = new Set(checks.map(c => `health:${c.name}`))
|
|
319
|
+
return {
|
|
320
|
+
name: 'health',
|
|
321
|
+
owns: key => keys.has(key),
|
|
322
|
+
async evaluate() {
|
|
323
|
+
const found: AlertCondition[] = []
|
|
324
|
+
for (const check of checks) {
|
|
325
|
+
let message: string | null = null
|
|
326
|
+
try {
|
|
327
|
+
const result = await check.check()
|
|
328
|
+
if (result.status === 'unhealthy') message = `${check.name} is unhealthy${result.message !== undefined ? `: ${result.message}` : ''}`
|
|
329
|
+
} catch (err) {
|
|
330
|
+
message = `${check.name} check threw: ${describeError(err).message}`
|
|
331
|
+
}
|
|
332
|
+
if (message !== null) found.push({ key: `health:${check.name}`, severity, message, subject: { entity: 'health', entityId: check.name } })
|
|
333
|
+
}
|
|
334
|
+
return found
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Raises `unit-fault:<unit>` for each systemd unit with an abnormal stop in the last `window` ms (rows written by
|
|
341
|
+
* `pipework alerts unit-fault`), with the count and last result; resolves once a unit has no fault in the window.
|
|
342
|
+
* `units` limits it to those units.
|
|
343
|
+
*/
|
|
344
|
+
export function unitFaultSource(options: { window: number; owner?: string; runbook?: string; severity?: AlertSeverity; units?: readonly string[]; database?: string }): AlertSource {
|
|
345
|
+
if (typeof options.window !== 'number' || !Number.isFinite(options.window) || options.window <= 0) {
|
|
346
|
+
throw new Error('[pipework] alerts.source.unitFault needs window to be a positive number of milliseconds.')
|
|
347
|
+
}
|
|
348
|
+
if (options.units !== undefined && options.units.length === 0) {
|
|
349
|
+
throw new Error('[pipework] alerts.source.unitFault units must not be empty; omit it to watch every unit.')
|
|
350
|
+
}
|
|
351
|
+
const units = options.units === undefined ? null : JSON.stringify(options.units)
|
|
352
|
+
const text = (v: unknown): string | null => v === null || v === undefined ? null : String(v)
|
|
353
|
+
return querySource({
|
|
354
|
+
name: 'unit-fault',
|
|
355
|
+
severity: options.severity ?? 'error',
|
|
356
|
+
...(options.database !== undefined ? { database: options.database } : {}),
|
|
357
|
+
...ownerFields(options),
|
|
358
|
+
query: sql`
|
|
359
|
+
SELECT unit, count(*)::int AS n,
|
|
360
|
+
(array_agg(result ORDER BY at DESC))[1] AS last_result,
|
|
361
|
+
(array_agg(exit_code ORDER BY at DESC))[1] AS last_exit_code,
|
|
362
|
+
(array_agg(exit_status ORDER BY at DESC))[1] AS last_exit_status,
|
|
363
|
+
min(at) AS first_at, max(at) AS last_at,
|
|
364
|
+
ceil(extract(epoch FROM clock_timestamp() - min(at)) / 60)::int AS minutes
|
|
365
|
+
FROM pipework_unit_faults
|
|
366
|
+
WHERE at > clock_timestamp() - ${options.window} * interval '1 millisecond'
|
|
367
|
+
AND (${units}::jsonb IS NULL OR unit IN (SELECT jsonb_array_elements_text(${units}::jsonb)))
|
|
368
|
+
GROUP BY unit ORDER BY unit`,
|
|
369
|
+
id: r => String(r['unit']),
|
|
370
|
+
message: r => `${String(r['unit'])}: ${String(r['last_result'])} x${String(Number(r['n']))} in ${String(Math.max(1, Number(r['minutes'])))} min`,
|
|
371
|
+
subject: r => ({ entity: 'unit', entityId: String(r['unit']) }),
|
|
372
|
+
context: r => ({
|
|
373
|
+
count: Number(r['n']),
|
|
374
|
+
lastResult: String(r['last_result']),
|
|
375
|
+
lastExitCode: text(r['last_exit_code']),
|
|
376
|
+
lastExitStatus: text(r['last_exit_status']),
|
|
377
|
+
firstAt: new Date(r['first_at'] as string | Date).toISOString(),
|
|
378
|
+
lastAt: new Date(r['last_at'] as string | Date).toISOString(),
|
|
379
|
+
}),
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const pad2 = (n: number): string => String(n).padStart(2, '0')
|
|
384
|
+
|
|
385
|
+
function untilText(reset: Date, now: Date): string {
|
|
386
|
+
const day = reset.toISOString().slice(0, 10)
|
|
387
|
+
const hhmm = `${pad2(reset.getUTCHours())}:${pad2(reset.getUTCMinutes())}Z`
|
|
388
|
+
return day === now.toISOString().slice(0, 10) ? hhmm : `${day} ${hhmm}`
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Raises `quota-exhausted:<key>` while an upstream quota row (needs `quota` configured) has remaining at or
|
|
393
|
+
* under `floor` (default 0) before its reset, or refused a call since it was last recorded; resolves when the
|
|
394
|
+
* row recovers or its reset passes. `keys` limits it to those keys (a non-empty array or a RegExp). Conditions
|
|
395
|
+
* carry `owner`, `runbook` and a context of `key`, `remaining`, `limit`, `resetAt` and `refused`.
|
|
396
|
+
*/
|
|
397
|
+
export function quotaExhaustedSource(options: { owner?: string; runbook?: string; keys?: readonly string[] | RegExp; floor?: number; severity?: AlertSeverity; database?: string } = {}): AlertSource {
|
|
398
|
+
const floor = options.floor ?? 0
|
|
399
|
+
if (!Number.isInteger(floor) || floor < 0) {
|
|
400
|
+
throw new Error(`[pipework] alerts.source.quotaExhausted floor must be a non-negative integer, got ${String(floor)}.`)
|
|
401
|
+
}
|
|
402
|
+
const keys = options.keys
|
|
403
|
+
if (Array.isArray(keys) && keys.length === 0) {
|
|
404
|
+
throw new Error('[pipework] alerts.source.quotaExhausted keys must not be empty; omit it to watch every key.')
|
|
405
|
+
}
|
|
406
|
+
const keyFilter = Array.isArray(keys)
|
|
407
|
+
? sql`AND key IN (SELECT jsonb_array_elements_text(${JSON.stringify([...new Set(keys as readonly string[])])}::jsonb))`
|
|
408
|
+
: sql``
|
|
409
|
+
const base = querySource({
|
|
410
|
+
name: 'quota-exhausted',
|
|
411
|
+
severity: options.severity ?? 'error',
|
|
412
|
+
...(options.database !== undefined ? { database: options.database } : {}),
|
|
413
|
+
...ownerFields(options),
|
|
414
|
+
query: sql`
|
|
415
|
+
SELECT key, remaining, "limit", reset_at,
|
|
416
|
+
(remaining > ${floor} AND refused_at IS NOT NULL AND refused_at >= updated_at) AS refused_only,
|
|
417
|
+
(refused_at IS NOT NULL AND refused_at >= updated_at) AS refused
|
|
418
|
+
FROM outbound_quota
|
|
419
|
+
WHERE reset_at > now() ${keyFilter}
|
|
420
|
+
AND (remaining <= ${floor} OR (refused_at IS NOT NULL AND refused_at >= updated_at))
|
|
421
|
+
ORDER BY key`,
|
|
422
|
+
id: r => String(r['key']),
|
|
423
|
+
message: r => {
|
|
424
|
+
const remaining = Number(r['remaining'])
|
|
425
|
+
const limit = r['limit'] === null ? null : Number(r['limit'])
|
|
426
|
+
const head = limit === null ? `${String(r['key'])} ${String(remaining)} remaining` : `${String(r['key'])} ${String(remaining)}/${String(limit)}`
|
|
427
|
+
return `${head} until ${untilText(new Date(r['reset_at'] as string | Date), new Date())}${r['refused_only'] === true ? '; a call was refused' : ''}`
|
|
428
|
+
},
|
|
429
|
+
subject: r => ({ entity: 'quota', entityId: String(r['key']) }),
|
|
430
|
+
context: r => ({
|
|
431
|
+
key: String(r['key']),
|
|
432
|
+
remaining: Number(r['remaining']),
|
|
433
|
+
limit: r['limit'] === null ? null : Number(r['limit']),
|
|
434
|
+
resetAt: new Date(r['reset_at'] as string | Date).toISOString(),
|
|
435
|
+
refused: r['refused'] === true,
|
|
436
|
+
}),
|
|
437
|
+
})
|
|
438
|
+
if (!(keys instanceof RegExp)) return base
|
|
439
|
+
return {
|
|
440
|
+
...base,
|
|
441
|
+
async evaluate() {
|
|
442
|
+
return (await base.evaluate()).filter(c => {
|
|
443
|
+
keys.lastIndex = 0
|
|
444
|
+
return keys.test(c.key.slice('quota-exhausted:'.length))
|
|
445
|
+
})
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Raises `loop-stalled:<name>` for each background loop (rows in `pipework_loop_beats`, written by the alerts tick,
|
|
452
|
+
* the job scheduler, reaper and claim loop, and the support events drain) that has not run, or not succeeded, within
|
|
453
|
+
* its allowance: `max(3 * its expected interval, 60 s)`, or `maxAge[name]` ms. `alerts.tick` is always left out, since
|
|
454
|
+
* a stopped tick cannot report itself; `pipework alerts stale` and `alerts.loopCheck` cover it from outside.
|
|
455
|
+
* `exclude` leaves other loops out. Conditions carry a context of `name`, `lastAt`, `lastOkAt`, `lastError`, `expectEveryMs` and `allowanceMs`.
|
|
456
|
+
*/
|
|
457
|
+
export function loopStalledSource(options: { maxAge?: Readonly<Record<string, number>>; owner?: string; runbook?: string; severity?: AlertSeverity; exclude?: readonly string[]; database?: string } = {}): AlertSource {
|
|
458
|
+
const severity = options.severity ?? 'error'
|
|
459
|
+
const exclude = [ALERTS_TICK_LOOP, ...(options.exclude ?? [])]
|
|
460
|
+
return {
|
|
461
|
+
name: 'loop-stalled',
|
|
462
|
+
owns: key => key.startsWith('loop-stalled:'),
|
|
463
|
+
async evaluate() {
|
|
464
|
+
const rows = await pipeSystem(options.database).execute(sql.raw(LOOP_BEATS_QUERY))
|
|
465
|
+
return staleLoops(rows, { ...(options.maxAge !== undefined ? { maxAge: options.maxAge } : {}), exclude }).map(l => ({
|
|
466
|
+
key: `loop-stalled:${l.name}`,
|
|
467
|
+
severity,
|
|
468
|
+
message: l.message,
|
|
469
|
+
subject: { entity: 'loop', entityId: l.name },
|
|
470
|
+
...(options.owner !== undefined ? { owner: options.owner } : {}),
|
|
471
|
+
...(options.runbook !== undefined ? { runbook: options.runbook } : {}),
|
|
472
|
+
context: { name: l.name, lastAt: l.lastAt, lastOkAt: l.lastOkAt, lastError: l.lastError, expectEveryMs: l.expectEveryMs, allowanceMs: l.allowanceMs },
|
|
473
|
+
}))
|
|
474
|
+
},
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* A health check named `loops` for `/ready`: unhealthy while any included loop (default all, `alerts.tick` too) is
|
|
480
|
+
* stale, with the loops and their ages in the message. Inside a managed context it uses that instance; for
|
|
481
|
+
* `/ready` pass the `instance` (the route runs outside one).
|
|
482
|
+
*/
|
|
483
|
+
export function loopCheck(options: { maxAge?: Readonly<Record<string, number>>; include?: readonly string[]; database?: string; instance?: Manifold } = {}): HealthCheck {
|
|
484
|
+
return {
|
|
485
|
+
name: 'loops',
|
|
486
|
+
async check() {
|
|
487
|
+
const instance = options.instance ?? getContext()?.instance
|
|
488
|
+
if (instance === undefined) {
|
|
489
|
+
throw new Error('[pipework] alerts.loopCheck needs { instance } when it runs outside a managed context (as /ready does).')
|
|
490
|
+
}
|
|
491
|
+
const name = options.database ?? instance.config.defaultDatabaseName()
|
|
492
|
+
const db = instance.pool.getOrCreate(instance.config.database(name)).drizzle
|
|
493
|
+
const rows = await db.execute(sql.raw(LOOP_BEATS_QUERY))
|
|
494
|
+
if (rows.length === 0) return { status: 'unhealthy', message: 'no loop has ever recorded a beat' }
|
|
495
|
+
const stale = staleLoops(rows, {
|
|
496
|
+
...(options.maxAge !== undefined ? { maxAge: options.maxAge } : {}),
|
|
497
|
+
...(options.include !== undefined ? { include: options.include } : {}),
|
|
498
|
+
})
|
|
499
|
+
if (stale.length === 0) return { status: 'healthy' }
|
|
500
|
+
return { status: 'unhealthy', message: stale.map(l => l.message).join('; ') }
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
}
|