@cosmicdrift/kumiko-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { observabilityContext } from "./context";
|
|
2
|
+
import { generateSpanId, generateTraceId } from "./ids";
|
|
3
|
+
import { redactAttributes, redactValue, shouldRedactAttribute } from "./sensitive-filter";
|
|
4
|
+
import type {
|
|
5
|
+
SensitiveFilterConfig,
|
|
6
|
+
SerializedTraceContext,
|
|
7
|
+
Span,
|
|
8
|
+
SpanAttributes,
|
|
9
|
+
SpanAttributeValue,
|
|
10
|
+
SpanKind,
|
|
11
|
+
SpanStatus,
|
|
12
|
+
StartSpanOptions,
|
|
13
|
+
Tracer,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
// A RecordedSpan is the internal representation that provider emitters
|
|
17
|
+
// (console, otlp, test-collector) operate on. Every field is plain data —
|
|
18
|
+
// no references to the tracer or provider — so recording is cheap and the
|
|
19
|
+
// data can be serialized.
|
|
20
|
+
export type RecordedSpan = {
|
|
21
|
+
readonly traceId: string;
|
|
22
|
+
readonly spanId: string;
|
|
23
|
+
readonly parentSpanId: string | undefined;
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly kind: SpanKind;
|
|
26
|
+
readonly startTime: number;
|
|
27
|
+
endTime: number | undefined;
|
|
28
|
+
attributes: Record<string, SpanAttributeValue>;
|
|
29
|
+
status: SpanStatus;
|
|
30
|
+
statusMessage: string | undefined;
|
|
31
|
+
exception: { readonly name: string; readonly message: string } | undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type SpanEndHandler = (span: RecordedSpan) => void;
|
|
35
|
+
|
|
36
|
+
class RecordingSpan implements Span {
|
|
37
|
+
readonly traceId: string;
|
|
38
|
+
readonly spanId: string;
|
|
39
|
+
readonly parentSpanId: string | undefined;
|
|
40
|
+
readonly name: string;
|
|
41
|
+
private readonly record: RecordedSpan;
|
|
42
|
+
private readonly sensitiveConfig: SensitiveFilterConfig;
|
|
43
|
+
private readonly onEnd: SpanEndHandler;
|
|
44
|
+
private _ended = false;
|
|
45
|
+
|
|
46
|
+
constructor(args: {
|
|
47
|
+
record: RecordedSpan;
|
|
48
|
+
sensitiveConfig: SensitiveFilterConfig;
|
|
49
|
+
onEnd: SpanEndHandler;
|
|
50
|
+
}) {
|
|
51
|
+
this.record = args.record;
|
|
52
|
+
this.sensitiveConfig = args.sensitiveConfig;
|
|
53
|
+
this.onEnd = args.onEnd;
|
|
54
|
+
this.traceId = args.record.traceId;
|
|
55
|
+
this.spanId = args.record.spanId;
|
|
56
|
+
this.parentSpanId = args.record.parentSpanId;
|
|
57
|
+
this.name = args.record.name;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setAttribute(key: string, value: SpanAttributeValue): void {
|
|
61
|
+
// skip: span ended — mutations after end() would race the emitted snapshot
|
|
62
|
+
if (this._ended) return;
|
|
63
|
+
this.record.attributes[key] = shouldRedactAttribute(key, this.sensitiveConfig)
|
|
64
|
+
? redactValue(value)
|
|
65
|
+
: value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setAttributes(attrs: SpanAttributes): void {
|
|
69
|
+
// skip: span ended — see setAttribute comment
|
|
70
|
+
if (this._ended) return;
|
|
71
|
+
const safe = redactAttributes(attrs, this.sensitiveConfig);
|
|
72
|
+
for (const [k, v] of Object.entries(safe)) {
|
|
73
|
+
this.record.attributes[k] = v;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setStatus(status: SpanStatus, message?: string): void {
|
|
78
|
+
// skip: span ended — status is already part of the emitted snapshot
|
|
79
|
+
if (this._ended) return;
|
|
80
|
+
this.record.status = status;
|
|
81
|
+
this.record.statusMessage = message;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
recordException(error: Error): void {
|
|
85
|
+
// skip: span ended — exception is already part of the emitted snapshot
|
|
86
|
+
if (this._ended) return;
|
|
87
|
+
this.record.exception = { name: error.name, message: error.message };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
end(endTime?: number): void {
|
|
91
|
+
// skip: double-end — onEnd should fire exactly once per span
|
|
92
|
+
if (this._ended) return;
|
|
93
|
+
this._ended = true;
|
|
94
|
+
this.record.endTime = endTime ?? performance.now();
|
|
95
|
+
this.onEnd(this.record);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get ended(): boolean {
|
|
99
|
+
return this._ended;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type RecordingTracerOptions = {
|
|
104
|
+
readonly sensitiveConfig: SensitiveFilterConfig;
|
|
105
|
+
// Called once per span after end(). Emitters (console, otlp) hook here.
|
|
106
|
+
readonly onSpanEnd: SpanEndHandler;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export class RecordingTracer implements Tracer {
|
|
110
|
+
private readonly sensitiveConfig: SensitiveFilterConfig;
|
|
111
|
+
private readonly onSpanEnd: SpanEndHandler;
|
|
112
|
+
|
|
113
|
+
constructor(opts: RecordingTracerOptions) {
|
|
114
|
+
this.sensitiveConfig = opts.sensitiveConfig;
|
|
115
|
+
this.onSpanEnd = opts.onSpanEnd;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
startSpan(name: string, options?: StartSpanOptions): Span {
|
|
119
|
+
// Parent resolution: explicit parent (Span or SerializedTraceContext —
|
|
120
|
+
// both carry traceId+spanId so a uniform read works) or the ALS active
|
|
121
|
+
// span. A missing parent starts a new trace.
|
|
122
|
+
const explicitParent = options?.parent;
|
|
123
|
+
const active = explicitParent ?? this.getActiveSpan();
|
|
124
|
+
const traceId = active?.traceId ?? generateTraceId();
|
|
125
|
+
const parentSpanId = active?.spanId;
|
|
126
|
+
|
|
127
|
+
const record: RecordedSpan = {
|
|
128
|
+
traceId,
|
|
129
|
+
spanId: generateSpanId(),
|
|
130
|
+
parentSpanId,
|
|
131
|
+
name,
|
|
132
|
+
kind: options?.kind ?? "internal",
|
|
133
|
+
startTime: options?.startTime ?? performance.now(),
|
|
134
|
+
endTime: undefined,
|
|
135
|
+
attributes: options?.attributes
|
|
136
|
+
? redactAttributes(options.attributes, this.sensitiveConfig)
|
|
137
|
+
: {},
|
|
138
|
+
status: "unset",
|
|
139
|
+
statusMessage: undefined,
|
|
140
|
+
exception: undefined,
|
|
141
|
+
};
|
|
142
|
+
return new RecordingSpan({
|
|
143
|
+
record,
|
|
144
|
+
sensitiveConfig: this.sensitiveConfig,
|
|
145
|
+
onEnd: this.onSpanEnd,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async withSpan<T>(
|
|
150
|
+
name: string,
|
|
151
|
+
optionsOrFn: StartSpanOptions | ((span: Span) => Promise<T>),
|
|
152
|
+
fn?: (span: Span) => Promise<T>,
|
|
153
|
+
): Promise<T> {
|
|
154
|
+
const options = typeof optionsOrFn === "function" ? {} : (optionsOrFn ?? {});
|
|
155
|
+
const actualFn = typeof optionsOrFn === "function" ? optionsOrFn : fn;
|
|
156
|
+
if (!actualFn) {
|
|
157
|
+
throw new Error("withSpan called without callback");
|
|
158
|
+
}
|
|
159
|
+
const span = this.startSpan(name, options);
|
|
160
|
+
try {
|
|
161
|
+
return await observabilityContext.run({ activeSpan: span }, () => actualFn(span));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error instanceof Error) {
|
|
164
|
+
span.recordException(error);
|
|
165
|
+
span.setStatus("error", error.message);
|
|
166
|
+
} else {
|
|
167
|
+
span.setStatus("error", String(error));
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
} finally {
|
|
171
|
+
if (!span.ended) span.end();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getActiveSpan(): Span | undefined {
|
|
176
|
+
return observabilityContext.getActiveSpan();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @deprecated Prefer `startSpan(name, { parent: context })`. Retained as a
|
|
181
|
+
* thin alias for call-sites that pre-date the unified StartSpanOptions.
|
|
182
|
+
*/
|
|
183
|
+
startSpanFromContext(
|
|
184
|
+
name: string,
|
|
185
|
+
context: SerializedTraceContext,
|
|
186
|
+
options?: StartSpanOptions,
|
|
187
|
+
): Span {
|
|
188
|
+
return this.startSpan(name, { ...options, parent: context });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Helper to serialize an active Span into the cross-process format.
|
|
193
|
+
export function serializeSpanContext(span: Span): SerializedTraceContext {
|
|
194
|
+
return {
|
|
195
|
+
traceId: span.traceId,
|
|
196
|
+
spanId: span.spanId,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type Redis from "ioredis";
|
|
2
|
+
import type { Tracer } from "./types";
|
|
3
|
+
|
|
4
|
+
// List of Redis commands we want to trace. Everything else (connection
|
|
5
|
+
// methods like `on`, `off`, `disconnect`, `duplicate`, plus pipeline/multi)
|
|
6
|
+
// falls through unwrapped. Keeping the list explicit avoids accidentally
|
|
7
|
+
// wrapping things that aren't commands — `connect` is a Promise, `on` is
|
|
8
|
+
// a listener registration — and keeps hot paths cheap.
|
|
9
|
+
const TRACKED_COMMANDS = new Set<string>([
|
|
10
|
+
// Strings / keys
|
|
11
|
+
"get",
|
|
12
|
+
"set",
|
|
13
|
+
"del",
|
|
14
|
+
"exists",
|
|
15
|
+
"expire",
|
|
16
|
+
"ttl",
|
|
17
|
+
"incr",
|
|
18
|
+
"decr",
|
|
19
|
+
"keys",
|
|
20
|
+
"scan",
|
|
21
|
+
"mget",
|
|
22
|
+
"mset",
|
|
23
|
+
// Hashes
|
|
24
|
+
"hget",
|
|
25
|
+
"hset",
|
|
26
|
+
"hmget",
|
|
27
|
+
"hmset",
|
|
28
|
+
"hgetall",
|
|
29
|
+
"hdel",
|
|
30
|
+
"hexists",
|
|
31
|
+
// Lists
|
|
32
|
+
"lpush",
|
|
33
|
+
"rpush",
|
|
34
|
+
"lpop",
|
|
35
|
+
"rpop",
|
|
36
|
+
"llen",
|
|
37
|
+
"lrange",
|
|
38
|
+
// Sets
|
|
39
|
+
"sadd",
|
|
40
|
+
"srem",
|
|
41
|
+
"smembers",
|
|
42
|
+
"sismember",
|
|
43
|
+
// Sorted sets
|
|
44
|
+
"zadd",
|
|
45
|
+
"zrange",
|
|
46
|
+
"zrem",
|
|
47
|
+
"zrangebyscore",
|
|
48
|
+
// Streams
|
|
49
|
+
"xadd",
|
|
50
|
+
"xread",
|
|
51
|
+
"xreadgroup",
|
|
52
|
+
"xack",
|
|
53
|
+
"xlen",
|
|
54
|
+
"xgroup",
|
|
55
|
+
"xdel",
|
|
56
|
+
"xrange",
|
|
57
|
+
"xpending",
|
|
58
|
+
// Pub/Sub
|
|
59
|
+
"publish",
|
|
60
|
+
"subscribe",
|
|
61
|
+
"unsubscribe",
|
|
62
|
+
"psubscribe",
|
|
63
|
+
"punsubscribe",
|
|
64
|
+
// Scripting
|
|
65
|
+
"eval",
|
|
66
|
+
"evalsha",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
// Extract a redaction-safe key pattern from the first command argument.
|
|
70
|
+
// Actual keys often include user-generated fragments (ids, session tokens)
|
|
71
|
+
// that shouldn't leak into traces — we replace everything after the second
|
|
72
|
+
// `:` segment with `*`. Known namespace conventions (colon-separated) keep
|
|
73
|
+
// enough signal for grouping without leaking values.
|
|
74
|
+
function extractKeyPattern(arg: unknown): string | undefined {
|
|
75
|
+
if (typeof arg !== "string") return undefined;
|
|
76
|
+
const parts = arg.split(":");
|
|
77
|
+
if (parts.length <= 2) return arg;
|
|
78
|
+
return `${parts.slice(0, 2).join(":")}:*`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Wrap an ioredis client in an observability-aware proxy. Traced commands
|
|
82
|
+
// emit `redis.cmd` spans; everything else is passed through so pipeline,
|
|
83
|
+
// transaction, and connection APIs keep working.
|
|
84
|
+
export function wrapRedisClient(client: Redis, tracer: Tracer): Redis {
|
|
85
|
+
return new Proxy(client, {
|
|
86
|
+
get(target, prop, _receiver) {
|
|
87
|
+
// Symbols (e.g. internal queue) pass through unchanged.
|
|
88
|
+
if (typeof prop !== "string") {
|
|
89
|
+
return Reflect.get(target, prop, target);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const original = Reflect.get(target, prop, target);
|
|
93
|
+
|
|
94
|
+
if (prop === "duplicate") {
|
|
95
|
+
// Preserve wrapping on duplicated connections.
|
|
96
|
+
return (...args: unknown[]) => {
|
|
97
|
+
// @cast-boundary engine-bridge — Reflect.get returns unknown, narrow to ioredis-method
|
|
98
|
+
const dup = (original as (...args: unknown[]) => Redis).apply(target, args);
|
|
99
|
+
return wrapRedisClient(dup, tracer);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (typeof original !== "function" || !TRACKED_COMMANDS.has(prop)) {
|
|
104
|
+
// Pass-through for non-command methods. Bind to target so `this`
|
|
105
|
+
// inside ioredis internals still works.
|
|
106
|
+
if (typeof original === "function") {
|
|
107
|
+
// @cast-boundary engine-bridge — Reflect.get returns unknown, narrow to ioredis-method
|
|
108
|
+
return (original as (...args: unknown[]) => unknown).bind(target);
|
|
109
|
+
}
|
|
110
|
+
return original;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return function wrappedCommand(this: unknown, ...args: unknown[]) {
|
|
114
|
+
const keyPattern = extractKeyPattern(args[0]);
|
|
115
|
+
return tracer.withSpan(
|
|
116
|
+
"redis.cmd",
|
|
117
|
+
{
|
|
118
|
+
kind: "client",
|
|
119
|
+
attributes: {
|
|
120
|
+
"redis.command": prop,
|
|
121
|
+
...(keyPattern ? { "redis.key_pattern": keyPattern } : {}),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
async () => {
|
|
125
|
+
// @cast-boundary engine-bridge — Reflect.get returns unknown, narrow to ioredis-method
|
|
126
|
+
return (original as (...args: unknown[]) => unknown).apply(target, args);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { SensitiveFilterConfig, SpanAttributeValue } from "./types";
|
|
2
|
+
|
|
3
|
+
export const REDACTED = "[REDACTED]";
|
|
4
|
+
|
|
5
|
+
// Defaults are intentionally conservative — easier to add a header to the
|
|
6
|
+
// list than to explain a PII leak. All matching is case-insensitive.
|
|
7
|
+
export const DEFAULT_SENSITIVE_CONFIG: SensitiveFilterConfig = {
|
|
8
|
+
redactedHeaders: ["authorization", "cookie", "set-cookie", "x-api-key", "proxy-authorization"],
|
|
9
|
+
redactedQueryParams: [
|
|
10
|
+
"token",
|
|
11
|
+
"access_token",
|
|
12
|
+
"refresh_token",
|
|
13
|
+
"password",
|
|
14
|
+
"secret",
|
|
15
|
+
"api_key",
|
|
16
|
+
"apikey",
|
|
17
|
+
],
|
|
18
|
+
redactedAttributeKeyPatterns: [
|
|
19
|
+
/password/i,
|
|
20
|
+
/secret/i,
|
|
21
|
+
/token/i,
|
|
22
|
+
/apikey/i,
|
|
23
|
+
/privatekey/i,
|
|
24
|
+
/credential/i,
|
|
25
|
+
/session/i,
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function mergeSensitiveConfig(
|
|
30
|
+
override: Partial<SensitiveFilterConfig> | undefined,
|
|
31
|
+
): SensitiveFilterConfig {
|
|
32
|
+
if (!override) return DEFAULT_SENSITIVE_CONFIG;
|
|
33
|
+
return {
|
|
34
|
+
redactedHeaders: override.redactedHeaders ?? DEFAULT_SENSITIVE_CONFIG.redactedHeaders,
|
|
35
|
+
redactedQueryParams:
|
|
36
|
+
override.redactedQueryParams ?? DEFAULT_SENSITIVE_CONFIG.redactedQueryParams,
|
|
37
|
+
redactedAttributeKeyPatterns:
|
|
38
|
+
override.redactedAttributeKeyPatterns ??
|
|
39
|
+
DEFAULT_SENSITIVE_CONFIG.redactedAttributeKeyPatterns,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function lowercaseSet(names: readonly string[]): ReadonlySet<string> {
|
|
44
|
+
return new Set(names.map((n) => n.toLowerCase()));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function redactHeaders(
|
|
48
|
+
headers: Readonly<Record<string, string>>,
|
|
49
|
+
config: SensitiveFilterConfig,
|
|
50
|
+
): Record<string, string> {
|
|
51
|
+
const redacted = lowercaseSet(config.redactedHeaders);
|
|
52
|
+
const out: Record<string, string> = {};
|
|
53
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
54
|
+
out[key] = redacted.has(key.toLowerCase()) ? REDACTED : value;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Redact sensitive query params in a URL or query string. Returns a new URL
|
|
60
|
+
// string with the same shape — path, fragment, and ordering preserved.
|
|
61
|
+
export function redactQueryString(input: string, config: SensitiveFilterConfig): string {
|
|
62
|
+
const redacted = lowercaseSet(config.redactedQueryParams);
|
|
63
|
+
// URL wants absolute; handle both absolute and path-only forms.
|
|
64
|
+
const hasScheme = /^[a-z][a-z0-9+\-.]*:/i.test(input);
|
|
65
|
+
const base = hasScheme ? undefined : "http://_internal";
|
|
66
|
+
const url = base ? new URL(input, base) : new URL(input);
|
|
67
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
68
|
+
if (redacted.has(key.toLowerCase())) {
|
|
69
|
+
url.searchParams.set(key, REDACTED);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!hasScheme) {
|
|
73
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
74
|
+
}
|
|
75
|
+
return url.toString();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check a single attribute key against the redaction patterns.
|
|
79
|
+
// Used by the Span implementation when setAttribute is called.
|
|
80
|
+
export function shouldRedactAttribute(key: string, config: SensitiveFilterConfig): boolean {
|
|
81
|
+
for (const pattern of config.redactedAttributeKeyPatterns) {
|
|
82
|
+
if (pattern.test(key)) return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Produce a type-preserving redacted value: numbers become 0, booleans become
|
|
88
|
+
// false, strings become "[REDACTED]". Keeps downstream consumers (exporters,
|
|
89
|
+
// dashboards) from having to deal with type drift — a histogram bucket that
|
|
90
|
+
// expected a number never suddenly sees a string.
|
|
91
|
+
export function redactValue(value: SpanAttributeValue): SpanAttributeValue {
|
|
92
|
+
if (typeof value === "number") return 0;
|
|
93
|
+
if (typeof value === "boolean") return false;
|
|
94
|
+
return REDACTED;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Filter a full attribute map, replacing matching keys with a type-preserving
|
|
98
|
+
// redacted value.
|
|
99
|
+
export function redactAttributes(
|
|
100
|
+
attrs: Readonly<Record<string, SpanAttributeValue>>,
|
|
101
|
+
config: SensitiveFilterConfig,
|
|
102
|
+
): Record<string, SpanAttributeValue> {
|
|
103
|
+
const out: Record<string, SpanAttributeValue> = {};
|
|
104
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
105
|
+
out[key] = shouldRedactAttribute(key, config) ? redactValue(value) : value;
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { Meter, MetricDefinition } from "./types";
|
|
2
|
+
|
|
3
|
+
// Framework-level metrics registered automatically at boot. Names match the
|
|
4
|
+
// Prometheus + OTel-friendly shape documented in observability-naming.md.
|
|
5
|
+
// Feature code never emits these — the Framework wires them from the HTTP
|
|
6
|
+
// middleware, dispatcher, and DB wrapper.
|
|
7
|
+
|
|
8
|
+
export const STANDARD_METRIC_DEFS: readonly MetricDefinition[] = [
|
|
9
|
+
{
|
|
10
|
+
name: "kumiko_http_requests_total",
|
|
11
|
+
type: "counter",
|
|
12
|
+
description: "HTTP requests counted by route, method, and status.",
|
|
13
|
+
labels: ["route", "method", "status"],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "kumiko_http_request_duration_seconds",
|
|
17
|
+
type: "histogram",
|
|
18
|
+
description: "HTTP request latency in seconds.",
|
|
19
|
+
labels: ["route", "method"],
|
|
20
|
+
buckets: [0.005, 0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "kumiko_dispatcher_handler_duration_seconds",
|
|
24
|
+
type: "histogram",
|
|
25
|
+
description: "Dispatcher handler latency in seconds.",
|
|
26
|
+
labels: ["handler", "success"],
|
|
27
|
+
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "kumiko_dispatcher_handler_errors_total",
|
|
31
|
+
type: "counter",
|
|
32
|
+
description: "Dispatcher handler errors by class.",
|
|
33
|
+
labels: ["handler", "error_class"],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "kumiko_db_query_duration_seconds",
|
|
37
|
+
type: "histogram",
|
|
38
|
+
description: "DB query latency in seconds.",
|
|
39
|
+
labels: ["operation", "table"],
|
|
40
|
+
buckets: [0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
|
|
41
|
+
},
|
|
42
|
+
// Projection-rebuild duration. Only emitted when a rebuild runs (ops-op),
|
|
43
|
+
// not continuously. Lag metric (continuous, live projections) is skipped
|
|
44
|
+
// for now — apply is synchronous, so lag is definitionally 0; a meaningful
|
|
45
|
+
// lag counter lands with async-apply in a future sprint.
|
|
46
|
+
{
|
|
47
|
+
name: "kumiko_projection_rebuild_duration_seconds",
|
|
48
|
+
type: "histogram",
|
|
49
|
+
description: "Duration of a full projection rebuild in seconds.",
|
|
50
|
+
labels: ["projection", "success"],
|
|
51
|
+
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5, 30, 120, 600],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "kumiko_projection_rebuild_events_total",
|
|
55
|
+
type: "counter",
|
|
56
|
+
description: "Events replayed during a projection rebuild.",
|
|
57
|
+
labels: ["projection"],
|
|
58
|
+
},
|
|
59
|
+
// Event-dispatcher per-consumer metrics. Lag is the primary ops signal:
|
|
60
|
+
// how many events sit between the consumer's cursor and events-head.
|
|
61
|
+
// A growing lag means the consumer can't keep up; zero means fully caught up.
|
|
62
|
+
// instance_id labels each shard: SHARED_INSTANCE_SENTINEL for shared
|
|
63
|
+
// consumers (one cursor globally), or the process-local instanceId for
|
|
64
|
+
// per-instance consumers (one cursor per dispatcher). Without this,
|
|
65
|
+
// Prometheus collapses per-instance shards into last-writer-wins and
|
|
66
|
+
// per-instance lag is invisible to alerting.
|
|
67
|
+
{
|
|
68
|
+
name: "kumiko_event_consumer_lag_events",
|
|
69
|
+
type: "gauge",
|
|
70
|
+
description: "Number of events between the consumer's cursor and the events head.",
|
|
71
|
+
labels: ["consumer", "instance_id"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "kumiko_event_consumer_events_processed_total",
|
|
75
|
+
type: "counter",
|
|
76
|
+
description: "Events successfully delivered to a consumer.",
|
|
77
|
+
labels: ["consumer", "instance_id"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "kumiko_event_consumer_events_failed_total",
|
|
81
|
+
type: "counter",
|
|
82
|
+
description:
|
|
83
|
+
"Event deliveries that threw. Repeated failures on the same event lead to dead-letter.",
|
|
84
|
+
labels: ["consumer", "instance_id"],
|
|
85
|
+
},
|
|
86
|
+
// LISTEN-subscription health. 1 = active, 0 = dropped. Drops to 0 while
|
|
87
|
+
// the dispatcher is running signal that delivery latency has regressed
|
|
88
|
+
// from sub-millisecond (LISTEN) to pollIntervalMs (timer fallback); ops
|
|
89
|
+
// should look at it the moment downstream latency SLOs start slipping.
|
|
90
|
+
{
|
|
91
|
+
name: "kumiko_event_dispatcher_listen_connected",
|
|
92
|
+
type: "gauge",
|
|
93
|
+
description:
|
|
94
|
+
"1 if the event-dispatcher holds an active PG LISTEN subscription on the events channel, 0 otherwise.",
|
|
95
|
+
labels: [],
|
|
96
|
+
},
|
|
97
|
+
] as const;
|
|
98
|
+
|
|
99
|
+
export function registerStandardMetrics(meter: Meter): void {
|
|
100
|
+
for (const def of STANDARD_METRIC_DEFS) {
|
|
101
|
+
// Guard: if already registered (e.g. buildServer called twice with the
|
|
102
|
+
// same meter instance — rare outside of hot-reload scenarios), skip.
|
|
103
|
+
if (meter.definitions().has(def.name)) continue;
|
|
104
|
+
meter.registerMetric(def);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Emit helpers — read-only surface for Auto-Instrumentation call sites.
|
|
109
|
+
// Using these instead of raw meter.counter("...").inc() keeps the metric
|
|
110
|
+
// names centralised.
|
|
111
|
+
|
|
112
|
+
export function emitHttpRequest(
|
|
113
|
+
meter: Meter,
|
|
114
|
+
labels: { readonly route: string; readonly method: string; readonly status: number },
|
|
115
|
+
durationSeconds: number,
|
|
116
|
+
): void {
|
|
117
|
+
meter.counter("kumiko_http_requests_total").inc(1, {
|
|
118
|
+
route: labels.route,
|
|
119
|
+
method: labels.method,
|
|
120
|
+
status: String(labels.status),
|
|
121
|
+
});
|
|
122
|
+
meter
|
|
123
|
+
.histogram("kumiko_http_request_duration_seconds")
|
|
124
|
+
.observe(durationSeconds, { route: labels.route, method: labels.method });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function emitDispatcherHandler(
|
|
128
|
+
meter: Meter,
|
|
129
|
+
labels: { readonly handler: string; readonly success: boolean },
|
|
130
|
+
durationSeconds: number,
|
|
131
|
+
): void {
|
|
132
|
+
meter.histogram("kumiko_dispatcher_handler_duration_seconds").observe(durationSeconds, {
|
|
133
|
+
handler: labels.handler,
|
|
134
|
+
success: String(labels.success),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function emitDispatcherError(
|
|
139
|
+
meter: Meter,
|
|
140
|
+
labels: { readonly handler: string; readonly errorClass: string },
|
|
141
|
+
): void {
|
|
142
|
+
meter.counter("kumiko_dispatcher_handler_errors_total").inc(1, {
|
|
143
|
+
handler: labels.handler,
|
|
144
|
+
error_class: labels.errorClass,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function emitDbQuery(
|
|
149
|
+
meter: Meter,
|
|
150
|
+
labels: { readonly operation: string; readonly table: string },
|
|
151
|
+
durationSeconds: number,
|
|
152
|
+
): void {
|
|
153
|
+
meter.histogram("kumiko_db_query_duration_seconds").observe(durationSeconds, {
|
|
154
|
+
operation: labels.operation,
|
|
155
|
+
table: labels.table,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function emitProjectionRebuild(
|
|
160
|
+
meter: Meter,
|
|
161
|
+
labels: { readonly projection: string; readonly success: boolean },
|
|
162
|
+
durationSeconds: number,
|
|
163
|
+
eventsReplayed: number,
|
|
164
|
+
): void {
|
|
165
|
+
meter.histogram("kumiko_projection_rebuild_duration_seconds").observe(durationSeconds, {
|
|
166
|
+
projection: labels.projection,
|
|
167
|
+
success: String(labels.success),
|
|
168
|
+
});
|
|
169
|
+
meter
|
|
170
|
+
.counter("kumiko_projection_rebuild_events_total")
|
|
171
|
+
.inc(eventsReplayed, { projection: labels.projection });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Per-shard consumer labels. `instance_id` is the reserved sentinel
|
|
175
|
+
// (SHARED_INSTANCE_SENTINEL) for shared-delivery consumers, and the
|
|
176
|
+
// concrete dispatcher-instanceId for per-instance shards. Labelling by
|
|
177
|
+
// both keeps Prometheus series distinct across instances so per-instance
|
|
178
|
+
// lag is visible — without it, scraped gauges collapse last-writer-wins
|
|
179
|
+
// and a single slow shard is invisible to alerting.
|
|
180
|
+
export function emitEventConsumerLag(
|
|
181
|
+
meter: Meter,
|
|
182
|
+
labels: { readonly consumer: string; readonly instanceId: string },
|
|
183
|
+
lagEvents: number,
|
|
184
|
+
): void {
|
|
185
|
+
meter.gauge("kumiko_event_consumer_lag_events").set(lagEvents, {
|
|
186
|
+
consumer: labels.consumer,
|
|
187
|
+
instance_id: labels.instanceId,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function emitEventDispatcherListenConnected(meter: Meter, connected: boolean): void {
|
|
192
|
+
meter.gauge("kumiko_event_dispatcher_listen_connected").set(connected ? 1 : 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function emitEventConsumerPassOutcome(
|
|
196
|
+
meter: Meter,
|
|
197
|
+
labels: { readonly consumer: string; readonly instanceId: string },
|
|
198
|
+
processed: number,
|
|
199
|
+
failed: number,
|
|
200
|
+
): void {
|
|
201
|
+
if (processed > 0) {
|
|
202
|
+
meter.counter("kumiko_event_consumer_events_processed_total").inc(processed, {
|
|
203
|
+
consumer: labels.consumer,
|
|
204
|
+
instance_id: labels.instanceId,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (failed > 0) {
|
|
208
|
+
meter.counter("kumiko_event_consumer_events_failed_total").inc(failed, {
|
|
209
|
+
consumer: labels.consumer,
|
|
210
|
+
instance_id: labels.instanceId,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Barrel for observability types. Split into span/metric/provider files so
|
|
2
|
+
// each module stays focused; consumers still import from "./types".
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
Counter,
|
|
6
|
+
Gauge,
|
|
7
|
+
Histogram,
|
|
8
|
+
Meter,
|
|
9
|
+
MetricDefinition,
|
|
10
|
+
MetricLabels,
|
|
11
|
+
MetricsHandle,
|
|
12
|
+
MetricType,
|
|
13
|
+
} from "./metric";
|
|
14
|
+
export type {
|
|
15
|
+
ObservabilityOptions,
|
|
16
|
+
ObservabilityProvider,
|
|
17
|
+
SamplingConfig,
|
|
18
|
+
SensitiveFilterConfig,
|
|
19
|
+
} from "./provider";
|
|
20
|
+
export type {
|
|
21
|
+
SerializedTraceContext,
|
|
22
|
+
Span,
|
|
23
|
+
SpanAttributes,
|
|
24
|
+
SpanAttributeValue,
|
|
25
|
+
SpanKind,
|
|
26
|
+
SpanStatus,
|
|
27
|
+
StartSpanOptions,
|
|
28
|
+
Tracer,
|
|
29
|
+
} from "./span";
|