@friggframework/core 2.0.0-next.11 → 2.0.0-next.110
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/CLAUDE.md +702 -0
- package/README.md +999 -50
- package/admin-scripts/index.js +52 -0
- package/admin-scripts/repositories/admin-script-execution-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-script-execution-repository-factory.js +51 -0
- package/admin-scripts/repositories/admin-script-execution-repository-interface.js +171 -0
- package/admin-scripts/repositories/admin-script-execution-repository-mongo.js +238 -0
- package/admin-scripts/repositories/admin-script-execution-repository-postgres.js +278 -0
- package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-schedule-repository-factory.js +51 -0
- package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
- package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
- package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
- package/application/commands/README.md +451 -0
- package/application/commands/admin-script-commands.js +359 -0
- package/application/commands/credential-commands.js +262 -0
- package/application/commands/entity-commands.js +336 -0
- package/application/commands/integration-commands.js +356 -0
- package/application/commands/integration-mapping-commands.js +25 -0
- package/application/commands/process-commands.js +135 -0
- package/application/commands/report-commands.js +188 -0
- package/application/commands/scheduler-commands.js +263 -0
- package/application/commands/usage-commands.js +56 -0
- package/application/commands/user-commands.js +312 -0
- package/application/index.js +83 -0
- package/artifacts/repositories/artifact-repository-factory.js +19 -0
- package/artifacts/repositories/artifact-repository-interface.js +27 -0
- package/artifacts/repositories/artifact-repository-local.js +42 -0
- package/artifacts/repositories/artifact-repository-s3.js +61 -0
- package/assertions/index.js +0 -3
- package/core/CLAUDE.md +700 -0
- package/core/Worker.js +60 -24
- package/core/create-handler.js +189 -5
- package/core/parameters-to-env.js +257 -0
- package/core/ssm-preload.mjs +34 -0
- package/credential/repositories/credential-active-type.js +32 -0
- package/credential/repositories/credential-repository-documentdb.js +355 -0
- package/credential/repositories/credential-repository-factory.js +54 -0
- package/credential/repositories/credential-repository-interface.js +113 -0
- package/credential/repositories/credential-repository-mongo.js +294 -0
- package/credential/repositories/credential-repository-postgres.js +312 -0
- package/credential/repositories/credential-repository.js +300 -0
- package/credential/use-cases/get-credential-for-user.js +25 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/MONGODB_TRANSACTION_FIX.md +198 -0
- package/database/adapters/lambda-invoker.js +97 -0
- package/database/config.js +154 -0
- package/database/documentdb-encryption-service.js +330 -0
- package/database/documentdb-utils.js +192 -0
- package/database/encryption/README.md +839 -0
- package/database/encryption/documentdb-encryption-service.md +3575 -0
- package/database/encryption/encryption-schema-registry.js +401 -0
- package/database/encryption/field-encryption-service.js +254 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/prisma-encryption-extension.js +230 -0
- package/database/index.js +21 -21
- package/database/prisma.js +182 -0
- package/database/repositories/health-check-repository-documentdb.js +138 -0
- package/database/repositories/health-check-repository-factory.js +48 -0
- package/database/repositories/health-check-repository-interface.js +82 -0
- package/database/repositories/health-check-repository-mongodb.js +89 -0
- package/database/repositories/health-check-repository-postgres.js +82 -0
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/use-cases/check-database-health-use-case.js +29 -0
- package/database/use-cases/check-database-state-use-case.js +81 -0
- package/database/use-cases/check-encryption-health-use-case.js +83 -0
- package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
- package/database/use-cases/get-migration-status-use-case.js +93 -0
- package/database/use-cases/resolve-migration-via-worker-use-case.js +49 -0
- package/database/use-cases/run-database-migration-use-case.js +139 -0
- package/database/use-cases/test-encryption-use-case.js +253 -0
- package/database/use-cases/trigger-database-migration-use-case.js +157 -0
- package/database/utils/mongodb-collection-utils.js +94 -0
- package/database/utils/mongodb-schema-init.js +108 -0
- package/database/utils/prisma-runner.js +491 -0
- package/database/utils/prisma-schema-parser.js +182 -0
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
- package/encrypt/Cryptor.js +34 -168
- package/encrypt/index.js +1 -2
- package/errors/client-safe-error.js +26 -0
- package/errors/fetch-error.js +15 -7
- package/errors/index.js +2 -0
- package/generated/prisma-mongodb/client.d.ts +1 -0
- package/generated/prisma-mongodb/client.js +5 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +5 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +386 -0
- package/generated/prisma-mongodb/index-browser.js +368 -0
- package/generated/prisma-mongodb/index.d.ts +27065 -0
- package/generated/prisma-mongodb/index.js +411 -0
- package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +35 -0
- package/generated/prisma-mongodb/runtime/edge.js +35 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +17 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
- package/generated/prisma-mongodb/runtime/library.js +147 -0
- package/generated/prisma-mongodb/runtime/react-native.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +85 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +38 -0
- package/generated/prisma-mongodb/schema.prisma +455 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +5 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +5 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +393 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +5 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +5 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +407 -0
- package/generated/prisma-postgresql/index-browser.js +389 -0
- package/generated/prisma-postgresql/index.d.ts +29904 -0
- package/generated/prisma-postgresql/index.js +432 -0
- package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-postgresql/package.json +183 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -0
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/edge-esm.js +35 -0
- package/generated/prisma-postgresql/runtime/edge.js +35 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +17 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
- package/generated/prisma-postgresql/runtime/library.js +147 -0
- package/generated/prisma-postgresql/runtime/react-native.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +85 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +38 -0
- package/generated/prisma-postgresql/schema.prisma +436 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +5 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +5 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +414 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/app-definition-loader.js +67 -0
- package/handlers/app-handler-helpers.js +3 -3
- package/handlers/backend-utils.js +271 -42
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/integration-event-dispatcher.js +68 -0
- package/handlers/middleware/admin-auth.js +73 -0
- package/handlers/routers/HEALTHCHECK.md +342 -0
- package/handlers/routers/auth.js +4 -15
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +329 -0
- package/handlers/routers/health.js +518 -0
- package/handlers/routers/integration-defined-routers.js +85 -10
- package/handlers/routers/integration-webhook-routers.js +80 -0
- package/handlers/routers/user.js +27 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
- package/handlers/workers/db-migration.js +427 -0
- package/handlers/workers/dlq-processor.js +63 -0
- package/handlers/workers/integration-defined-workers.js +13 -7
- package/index.js +102 -46
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
- package/infrastructure/scheduler/index.js +33 -0
- package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
- package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
- package/integrations/EXTENSIONS.md +240 -0
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/extension.js +254 -0
- package/integrations/index.js +20 -10
- package/integrations/integration-base.js +730 -74
- package/integrations/integration-router.js +398 -180
- package/integrations/options.js +1 -1
- package/integrations/repositories/config-patch-shared.js +43 -0
- package/integrations/repositories/integration-mapping-repository-documentdb.js +303 -0
- package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +120 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +183 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +255 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-documentdb.js +322 -0
- package/integrations/repositories/integration-repository-factory.js +51 -0
- package/integrations/repositories/integration-repository-interface.js +189 -0
- package/integrations/repositories/integration-repository-mongo.js +450 -0
- package/integrations/repositories/integration-repository-postgres.js +492 -0
- package/integrations/repositories/process-repository-documentdb.js +311 -0
- package/integrations/repositories/process-repository-factory.js +53 -0
- package/integrations/repositories/process-repository-interface.js +136 -0
- package/integrations/repositories/process-repository-mongo.js +262 -0
- package/integrations/repositories/process-repository-postgres.js +380 -0
- package/integrations/repositories/process-update-ops-shared.js +112 -0
- package/integrations/repositories/report-id.js +13 -0
- package/integrations/tests/doubles/config-capturing-integration.js +81 -0
- package/integrations/tests/doubles/dummy-integration-class.js +113 -0
- package/integrations/tests/doubles/test-integration-repository.js +146 -0
- package/integrations/use-cases/create-integration.js +215 -0
- package/integrations/use-cases/create-process.js +130 -0
- package/integrations/use-cases/delete-integration-for-user.js +119 -0
- package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +88 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/get-process.js +89 -0
- package/integrations/use-cases/index.js +19 -0
- package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/patch-integration-config.js +39 -0
- package/integrations/use-cases/process-errors.js +28 -0
- package/integrations/use-cases/update-integration-config.js +32 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +92 -0
- package/integrations/use-cases/update-process-metrics.js +222 -0
- package/integrations/use-cases/update-process-state.js +163 -0
- package/integrations/utils/map-integration-dto.js +37 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/logs/logger.js +0 -4
- package/{module-plugin → modules}/index.js +0 -10
- package/modules/module-factory.js +56 -0
- package/modules/module.js +307 -0
- package/modules/repositories/module-repository-documentdb.js +350 -0
- package/modules/repositories/module-repository-factory.js +40 -0
- package/modules/repositories/module-repository-interface.js +145 -0
- package/modules/repositories/module-repository-mongo.js +436 -0
- package/modules/repositories/module-repository-postgres.js +481 -0
- package/modules/repositories/module-repository.js +369 -0
- package/modules/requester/api-key.js +52 -0
- package/modules/requester/oauth-2.js +555 -0
- package/modules/requester/requester.js +542 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +71 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +34 -0
- package/modules/use-cases/get-module.js +74 -0
- package/modules/use-cases/process-authorization-callback.js +243 -0
- package/modules/use-cases/refresh-entity-options.js +72 -0
- package/modules/use-cases/test-module-auth.js +72 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +92 -50
- package/prisma-mongodb/schema.prisma +455 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
- package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
- package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
- package/prisma-postgresql/migrations/20260625000000_add_user_organization_id_index/migration.sql +2 -0
- package/prisma-postgresql/migrations/20260703000000_add_integration_status_in_creation_in_deletion/migration.sql +11 -0
- package/prisma-postgresql/migrations/20260703000001_integration_status_default_in_creation/migration.sql +8 -0
- package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +26 -0
- package/prisma-postgresql/migrations/20260706000000_add_admin_script_execution_and_schedule/migration.sql +61 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +436 -0
- package/queues/queuer-util.js +103 -21
- package/reporting/README.md +154 -0
- package/reporting/builtin-reports.js +6 -0
- package/reporting/index.js +13 -0
- package/reporting/report-base.js +49 -0
- package/reporting/reports/integrations-report.js +221 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-documentdb.js +240 -0
- package/syncs/repositories/sync-repository-factory.js +43 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/telemetry/README.md +331 -0
- package/telemetry/bind-telemetry-context.js +73 -0
- package/telemetry/canonical-counters.js +52 -0
- package/telemetry/exporters.js +85 -0
- package/telemetry/index.js +26 -0
- package/telemetry/instrument-handler.js +87 -0
- package/telemetry/no-op-telemetry.js +67 -0
- package/telemetry/north-star.js +103 -0
- package/telemetry/otel-telemetry.js +213 -0
- package/telemetry/plugin-subscribers.js +77 -0
- package/telemetry/telemetry-config.js +120 -0
- package/telemetry/telemetry-context.js +40 -0
- package/telemetry/telemetry-event-bus.js +58 -0
- package/telemetry/telemetry-runtime.js +147 -0
- package/telemetry/telemetry-service.js +51 -0
- package/telemetry/usage-rollup-subscriber.js +116 -0
- package/token/repositories/token-repository-documentdb.js +137 -0
- package/token/repositories/token-repository-factory.js +40 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +219 -0
- package/token/repositories/token-repository-postgres.js +264 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/associations/index.d.ts +0 -17
- package/types/core/index.d.ts +12 -4
- package/types/database/index.d.ts +10 -2
- package/types/encrypt/index.d.ts +5 -3
- package/types/integrations/index.d.ts +3 -8
- package/types/module-plugin/index.d.ts +20 -69
- package/types/syncs/index.d.ts +0 -17
- package/usage/README.md +54 -0
- package/usage/index.js +17 -0
- package/usage/repositories/usage-repository-documentdb.js +194 -0
- package/usage/repositories/usage-repository-factory.js +25 -0
- package/usage/repositories/usage-repository-interface.js +37 -0
- package/usage/repositories/usage-repository-prisma.js +146 -0
- package/usage/tracked-metrics.js +38 -0
- package/usage/usage-windows.js +24 -0
- package/user/repositories/user-repository-documentdb.js +458 -0
- package/user/repositories/user-repository-factory.js +52 -0
- package/user/repositories/user-repository-interface.js +214 -0
- package/user/repositories/user-repository-mongo.js +323 -0
- package/user/repositories/user-repository-postgres.js +377 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/use-cases/authenticate-user.js +127 -0
- package/user/use-cases/authenticate-with-shared-secret.js +48 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +125 -0
- package/utils/backend-path.js +38 -0
- package/utils/index.js +6 -0
- package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
- package/websocket/repositories/websocket-connection-repository.js +161 -0
- package/assertions/is-equal.js +0 -17
- package/associations/model.js +0 -54
- package/database/models/IndividualUser.js +0 -76
- package/database/models/OrganizationUser.js +0 -29
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/models/UserModel.js +0 -7
- package/database/models/WebsocketConnection.js +0 -49
- package/database/mongo.js +0 -45
- package/database/mongoose.js +0 -5
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -132
- package/encrypt/encrypt.test.js +0 -1069
- package/encrypt/test-encrypt.js +0 -107
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/entity.js +0 -46
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/requester/api-key.js +0 -36
- package/module-plugin/requester/oauth-2.js +0 -219
- package/module-plugin/requester/requester.js +0 -165
- package/module-plugin/requester/requester.test.js +0 -28
- package/module-plugin/test/auther.test.js +0 -97
- package/syncs/model.js +0 -62
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const otelApi = require('@opentelemetry/api');
|
|
2
|
+
const {
|
|
3
|
+
BasicTracerProvider,
|
|
4
|
+
BatchSpanProcessor,
|
|
5
|
+
ParentBasedSampler,
|
|
6
|
+
TraceIdRatioBasedSampler,
|
|
7
|
+
AlwaysOnSampler,
|
|
8
|
+
} = require('@opentelemetry/sdk-trace-base');
|
|
9
|
+
const {
|
|
10
|
+
MeterProvider,
|
|
11
|
+
PeriodicExportingMetricReader,
|
|
12
|
+
} = require('@opentelemetry/sdk-metrics');
|
|
13
|
+
const { resourceFromAttributes } = require('@opentelemetry/resources');
|
|
14
|
+
const { resolveExporter } = require('./exporters');
|
|
15
|
+
const { createTelemetryEventBus } = require('./telemetry-event-bus');
|
|
16
|
+
const {
|
|
17
|
+
runWithTelemetryContext,
|
|
18
|
+
mergeTelemetryContext,
|
|
19
|
+
} = require('./telemetry-context');
|
|
20
|
+
|
|
21
|
+
const TRACER_NAME = 'frigg';
|
|
22
|
+
const METRIC_EXPORT_INTERVAL_MS =
|
|
23
|
+
Number(process.env.OTEL_METRIC_EXPORT_INTERVAL_MS) || 60000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register an AsyncLocalStorage-backed OTel context manager once per process.
|
|
27
|
+
* Without it `BasicTracerProvider` uses the Noop context manager, so
|
|
28
|
+
* `startActiveSpan` never sets an active context — spans would be unparented
|
|
29
|
+
* roots, `getActiveSpan()` always undefined, and baggage inert. Guarded so
|
|
30
|
+
* repeated telemetry construction (tests) doesn't re-register.
|
|
31
|
+
*/
|
|
32
|
+
let contextManagerRegistered = false;
|
|
33
|
+
function ensureContextManager() {
|
|
34
|
+
if (contextManagerRegistered) return;
|
|
35
|
+
contextManagerRegistered = true;
|
|
36
|
+
try {
|
|
37
|
+
const {
|
|
38
|
+
AsyncLocalStorageContextManager,
|
|
39
|
+
} = require('@opentelemetry/context-async-hooks');
|
|
40
|
+
const manager = new AsyncLocalStorageContextManager();
|
|
41
|
+
manager.enable();
|
|
42
|
+
otelApi.context.setGlobalContextManager(manager);
|
|
43
|
+
} catch (_) {
|
|
44
|
+
// If registration fails, traces are flat but usage attribution (which
|
|
45
|
+
// rides the separate telemetry-context ALS) is unaffected.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* OpenTelemetry-backed telemetry adapter. Constructed only when a real exporter
|
|
51
|
+
* is configured (`createTelemetry` returns the no-op otherwise). `BatchSpanProcessor`
|
|
52
|
+
* is used uniformly — spans are delivered by the bounded `forceFlush()` the Lambda
|
|
53
|
+
* handler awaits before the container freezes (`callbackWaitsForEmptyEventLoop=false`),
|
|
54
|
+
* never by background timers.
|
|
55
|
+
*/
|
|
56
|
+
class OtelTelemetry {
|
|
57
|
+
constructor({
|
|
58
|
+
exporter,
|
|
59
|
+
resource = {},
|
|
60
|
+
sampleRatio,
|
|
61
|
+
bus = createTelemetryEventBus(),
|
|
62
|
+
} = {}) {
|
|
63
|
+
ensureContextManager();
|
|
64
|
+
const { traceExporter, metricExporter } = resolveExporter(exporter);
|
|
65
|
+
|
|
66
|
+
const resourceAttrs = { 'service.name': resource.service || 'frigg' };
|
|
67
|
+
if (resource.stage) {
|
|
68
|
+
resourceAttrs['deployment.environment'] = resource.stage;
|
|
69
|
+
resourceAttrs.stage = resource.stage;
|
|
70
|
+
}
|
|
71
|
+
if (resource.appName) resourceAttrs.appName = resource.appName;
|
|
72
|
+
const res = resourceFromAttributes(resourceAttrs);
|
|
73
|
+
|
|
74
|
+
const sampler =
|
|
75
|
+
typeof sampleRatio === 'number' && sampleRatio < 1
|
|
76
|
+
? new ParentBasedSampler({
|
|
77
|
+
root: new TraceIdRatioBasedSampler(sampleRatio),
|
|
78
|
+
})
|
|
79
|
+
: new AlwaysOnSampler();
|
|
80
|
+
|
|
81
|
+
this._tracerProvider = new BasicTracerProvider({
|
|
82
|
+
resource: res,
|
|
83
|
+
sampler,
|
|
84
|
+
spanProcessors: traceExporter
|
|
85
|
+
? [new BatchSpanProcessor(traceExporter)]
|
|
86
|
+
: [],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const metricReader = metricExporter
|
|
90
|
+
? new PeriodicExportingMetricReader({
|
|
91
|
+
exporter: metricExporter,
|
|
92
|
+
exportIntervalMillis: METRIC_EXPORT_INTERVAL_MS,
|
|
93
|
+
})
|
|
94
|
+
: null;
|
|
95
|
+
this._meterProvider = new MeterProvider({
|
|
96
|
+
resource: res,
|
|
97
|
+
readers: metricReader ? [metricReader] : [],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this._bus = bus;
|
|
101
|
+
this._tracer = this._tracerProvider.getTracer(TRACER_NAME);
|
|
102
|
+
this._meter = this._meterProvider.getMeter(TRACER_NAME);
|
|
103
|
+
// One instrument instance per metric name — OTel requires it, and
|
|
104
|
+
// re-creating a counter would drop data points.
|
|
105
|
+
this._counters = new Map();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_getCounter(name) {
|
|
109
|
+
if (!this._counters.has(name)) {
|
|
110
|
+
this._counters.set(name, this._meter.createCounter(name));
|
|
111
|
+
}
|
|
112
|
+
return this._counters.get(name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
count(name, value = 1, attributes = {}, context) {
|
|
116
|
+
try {
|
|
117
|
+
this._getCounter(name).add(value, attributes);
|
|
118
|
+
} catch (_) {
|
|
119
|
+
// Telemetry must never break the wrapped path.
|
|
120
|
+
}
|
|
121
|
+
// Mirror onto the internal stream for the usage rollup + plugin taps; the
|
|
122
|
+
// bus context (incl. high-cardinality ids) rides the bus only, never the
|
|
123
|
+
// OTel metric attributes.
|
|
124
|
+
const merged = mergeTelemetryContext(context);
|
|
125
|
+
const payload = { name, value, attributes };
|
|
126
|
+
if (merged) payload.context = merged;
|
|
127
|
+
this._bus.emit('metric', payload);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
event(name, attributes = {}, context) {
|
|
131
|
+
try {
|
|
132
|
+
const active = otelApi.trace.getActiveSpan();
|
|
133
|
+
if (active) active.addEvent(name, attributes);
|
|
134
|
+
} catch (_) {}
|
|
135
|
+
const merged = mergeTelemetryContext(context);
|
|
136
|
+
const payload = { name, attributes };
|
|
137
|
+
if (merged) payload.context = merged;
|
|
138
|
+
this._bus.emit('event', payload);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
startSpan(name, options) {
|
|
142
|
+
return this._tracer.startSpan(name, options);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async span(name, fn, options = {}) {
|
|
146
|
+
return this._tracer.startActiveSpan(name, options, async (span) => {
|
|
147
|
+
try {
|
|
148
|
+
const result =
|
|
149
|
+
typeof fn === 'function' ? await fn(span) : undefined;
|
|
150
|
+
span.setStatus({ code: otelApi.SpanStatusCode.OK });
|
|
151
|
+
return result;
|
|
152
|
+
} catch (err) {
|
|
153
|
+
span.recordException(err);
|
|
154
|
+
span.setStatus({
|
|
155
|
+
code: otelApi.SpanStatusCode.ERROR,
|
|
156
|
+
message: err && err.message,
|
|
157
|
+
});
|
|
158
|
+
throw err;
|
|
159
|
+
} finally {
|
|
160
|
+
span.end();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Run `fn` with the given identifiers on (a) the AsyncLocalStorage telemetry
|
|
167
|
+
* context — which the usage rollup reads to attribute emissions per-integration
|
|
168
|
+
* on ANY path — and (b) OTel baggage for trace propagation. High-cardinality
|
|
169
|
+
* ids ride here, never on metric labels.
|
|
170
|
+
*/
|
|
171
|
+
async withContext(attributes = {}, fn) {
|
|
172
|
+
const entries = {};
|
|
173
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
174
|
+
if (value !== undefined && value !== null) {
|
|
175
|
+
entries[key] = { value: String(value) };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const baggage = otelApi.propagation.createBaggage(entries);
|
|
179
|
+
const ctx = otelApi.propagation.setBaggage(
|
|
180
|
+
otelApi.context.active(),
|
|
181
|
+
baggage
|
|
182
|
+
);
|
|
183
|
+
return runWithTelemetryContext(attributes, () =>
|
|
184
|
+
otelApi.context.with(ctx, () =>
|
|
185
|
+
typeof fn === 'function' ? fn() : undefined
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
on(eventType, callback) {
|
|
191
|
+
return this._bus.on(eventType, callback);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async forceFlush() {
|
|
195
|
+
await Promise.allSettled([
|
|
196
|
+
this._tracerProvider.forceFlush(),
|
|
197
|
+
this._meterProvider.forceFlush(),
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async shutdown() {
|
|
202
|
+
await Promise.allSettled([
|
|
203
|
+
this._tracerProvider.shutdown(),
|
|
204
|
+
this._meterProvider.shutdown(),
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
isEnabled() {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = { OtelTelemetry };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative plugin/extension telemetry taps.
|
|
3
|
+
*
|
|
4
|
+
* The built-in usage rollup and North Star derivation subscribe to the telemetry
|
|
5
|
+
* bus in framework code; this lets an ADOPTER subscribe declaratively from the
|
|
6
|
+
* app definition (`Definition.telemetry.subscribers: [...]`) — forward to a
|
|
7
|
+
* custom sink, compute aggregates, persist counters — without writing wiring code.
|
|
8
|
+
*
|
|
9
|
+
* A subscriber is one of:
|
|
10
|
+
* - a **factory** `fn(telemetry)` that registers itself (e.g. calls
|
|
11
|
+
* `telemetry.on('metric', ...)`) and MAY return an unsubscribe function; or
|
|
12
|
+
* - a **declarative object** `{ event?: 'metric'|'event', handler(payload, eventType) }`
|
|
13
|
+
* — when `event` is omitted the handler receives both metric and event payloads.
|
|
14
|
+
*
|
|
15
|
+
* Every attach and every handler invocation is guarded so a misbehaving
|
|
16
|
+
* subscriber can never break emission, its siblings, or a handler (mirrors the
|
|
17
|
+
* bus's own per-subscriber isolation).
|
|
18
|
+
*
|
|
19
|
+
* @param {object} params
|
|
20
|
+
* @param {object} params.telemetry Telemetry service exposing `on(eventType, cb)`.
|
|
21
|
+
* @param {Array<Function|{event?: string, handler: Function}>} [params.subscribers]
|
|
22
|
+
* @returns {Array<Function>} unsubscribe functions for the wired subscribers.
|
|
23
|
+
*/
|
|
24
|
+
const EVENT_TYPES = ['metric', 'event'];
|
|
25
|
+
|
|
26
|
+
function wireTelemetrySubscribers({ telemetry, subscribers = [] } = {}) {
|
|
27
|
+
if (
|
|
28
|
+
!telemetry ||
|
|
29
|
+
typeof telemetry.on !== 'function' ||
|
|
30
|
+
!Array.isArray(subscribers)
|
|
31
|
+
) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const unsubscribes = [];
|
|
36
|
+
|
|
37
|
+
for (const subscriber of subscribers) {
|
|
38
|
+
try {
|
|
39
|
+
if (typeof subscriber === 'function') {
|
|
40
|
+
const off = subscriber(telemetry);
|
|
41
|
+
if (typeof off === 'function') unsubscribes.push(off);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (subscriber && typeof subscriber.handler === 'function') {
|
|
46
|
+
const events = subscriber.event
|
|
47
|
+
? [subscriber.event]
|
|
48
|
+
: EVENT_TYPES;
|
|
49
|
+
for (const eventType of events) {
|
|
50
|
+
const off = telemetry.on(eventType, (payload) => {
|
|
51
|
+
try {
|
|
52
|
+
subscriber.handler(payload, eventType);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn(
|
|
55
|
+
`[Frigg][telemetry] subscriber handler for "${eventType}" threw: ${
|
|
56
|
+
error && error.message
|
|
57
|
+
}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (typeof off === 'function') unsubscribes.push(off);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// A single bad subscriber must never break wiring of the others.
|
|
66
|
+
console.warn(
|
|
67
|
+
`[Frigg][telemetry] failed to wire a telemetry subscriber: ${
|
|
68
|
+
error && error.message
|
|
69
|
+
}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return unsubscribes;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { wireTelemetrySubscribers };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves and validates the telemetry section of an app definition
|
|
3
|
+
* into a normalized config the TelemetryService and
|
|
4
|
+
* usage rollup consume: `{ exporter, northStar, sampleRatio }`.
|
|
5
|
+
*
|
|
6
|
+
* Exporter default is keyed off STAGE: only a genuinely LOCAL run (STAGE=local)
|
|
7
|
+
* gets `console` (visible, free); every deployed stage — including `dev` —
|
|
8
|
+
* defaults to `none`. `dev` is a deployed AWS stage, and `console` there would
|
|
9
|
+
* write full spans to CloudWatch by default (cost + a data-exposure surface).
|
|
10
|
+
* Adopters opt into `console`/`otlp` explicitly for deployed stages.
|
|
11
|
+
*/
|
|
12
|
+
const VALID_EXPORTER_TYPES = [
|
|
13
|
+
'none',
|
|
14
|
+
'noop',
|
|
15
|
+
'console',
|
|
16
|
+
'otlp',
|
|
17
|
+
'honeycomb',
|
|
18
|
+
'datadog',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function normalizeExporterConfig(exporter, stage) {
|
|
22
|
+
if (!exporter) {
|
|
23
|
+
return stage === 'local' ? { type: 'console' } : { type: 'none' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Pre-built exporter instances (tests / advanced adopters) bypass type checks.
|
|
27
|
+
if (exporter.traceExporter || exporter.metricExporter) {
|
|
28
|
+
return exporter;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!VALID_EXPORTER_TYPES.includes(exporter.type)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`[Frigg][telemetry] invalid exporter type "${exporter.type}". ` +
|
|
34
|
+
`Supported: ${VALID_EXPORTER_TYPES.join(', ')}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return exporter;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fraction of TRACES exported (0..1, default 1) — a cost knob wired to the OTel
|
|
41
|
+
// sampler in otel-telemetry. Does NOT thin usage counters; those stay exact.
|
|
42
|
+
function resolveSampleRatio(sampleRatio) {
|
|
43
|
+
if (sampleRatio === undefined || sampleRatio === null) return 1;
|
|
44
|
+
if (
|
|
45
|
+
typeof sampleRatio !== 'number' ||
|
|
46
|
+
Number.isNaN(sampleRatio) ||
|
|
47
|
+
sampleRatio < 0 ||
|
|
48
|
+
sampleRatio > 1
|
|
49
|
+
) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[Frigg][telemetry] sampleRatio must be a number in [0,1], got ${sampleRatio}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return sampleRatio;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateNorthStarEntry(entry, where) {
|
|
58
|
+
if (!entry || typeof entry.name !== 'string' || !entry.name) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`[Frigg][telemetry] northStar.${where} must reference a counter { name }`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Adopter-declared telemetry subscribers. Each is either a factory
|
|
67
|
+
* function `fn(telemetry)` or a declarative `{ event?, handler }` object. Kept as
|
|
68
|
+
* an array so `wireTelemetrySubscribers` can attach them to the bus at runtime.
|
|
69
|
+
*/
|
|
70
|
+
function resolveSubscribers(subscribers) {
|
|
71
|
+
if (subscribers === undefined || subscribers === null) return [];
|
|
72
|
+
if (!Array.isArray(subscribers)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'[Frigg][telemetry] subscribers must be an array of functions or { handler } objects'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
for (const subscriber of subscribers) {
|
|
78
|
+
const ok =
|
|
79
|
+
typeof subscriber === 'function' ||
|
|
80
|
+
(subscriber && typeof subscriber.handler === 'function');
|
|
81
|
+
if (!ok) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'[Frigg][telemetry] each subscriber must be a function or an object with a handler function'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return subscribers;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveNorthStar(northStar) {
|
|
91
|
+
if (!northStar) return null;
|
|
92
|
+
if (northStar.default) validateNorthStarEntry(northStar.default, 'default');
|
|
93
|
+
if (northStar.byType) {
|
|
94
|
+
for (const [type, entry] of Object.entries(northStar.byType)) {
|
|
95
|
+
validateNorthStarEntry(entry, `byType.${type}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return northStar;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {object} appDefinition The backend `Definition` export.
|
|
103
|
+
* @param {object} [ctx]
|
|
104
|
+
* @param {string} [ctx.stage] Deployment stage (defaults to STAGE/NODE_ENV).
|
|
105
|
+
* @returns {{ exporter: object, northStar: object|null, sampleRatio: number }}
|
|
106
|
+
*/
|
|
107
|
+
function resolveTelemetryConfig(appDefinition = {}, ctx = {}) {
|
|
108
|
+
const stage =
|
|
109
|
+
ctx.stage || process.env.STAGE || process.env.NODE_ENV || 'production';
|
|
110
|
+
const telemetry = appDefinition.telemetry || {};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
exporter: normalizeExporterConfig(telemetry.exporter, stage),
|
|
114
|
+
northStar: resolveNorthStar(telemetry.northStar),
|
|
115
|
+
sampleRatio: resolveSampleRatio(telemetry.sampleRatio),
|
|
116
|
+
subscribers: resolveSubscribers(telemetry.subscribers),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { resolveTelemetryConfig };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ambient telemetry context. A single process-wide AsyncLocalStorage
|
|
5
|
+
* holds the standard identifiers ({integrationId, integrationType, userId,
|
|
6
|
+
* version, ...}) for the duration of a handler, set once at the handler seam via
|
|
7
|
+
* `telemetry.withContext(...)`. Any emission during that async scope — including
|
|
8
|
+
* deep calls like an API-module request — reads the ambient context, so the
|
|
9
|
+
* usage rollup can attribute counters per-integration WITHOUT threading context
|
|
10
|
+
* through every call and WITHOUT relying on OTel baggage (which does not exist
|
|
11
|
+
* on the no-op path). This is the propagation mechanism the plan intended.
|
|
12
|
+
*
|
|
13
|
+
* Only bus-payload `context` is populated from here; high-cardinality ids never
|
|
14
|
+
* touch OTel metric labels.
|
|
15
|
+
*/
|
|
16
|
+
const store = new AsyncLocalStorage();
|
|
17
|
+
|
|
18
|
+
function runWithTelemetryContext(context, fn) {
|
|
19
|
+
return store.run(context || {}, fn);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getTelemetryContextStore() {
|
|
23
|
+
return store.getStore() || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Merge the ambient context with an explicit per-call context (explicit keys
|
|
28
|
+
* win, e.g. a request `url`). Returns undefined when there is nothing to attach.
|
|
29
|
+
*/
|
|
30
|
+
function mergeTelemetryContext(explicit) {
|
|
31
|
+
const ambient = store.getStore();
|
|
32
|
+
if (!ambient && !explicit) return undefined;
|
|
33
|
+
return { ...(ambient || {}), ...(explicit || {}) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
runWithTelemetryContext,
|
|
38
|
+
getTelemetryContextStore,
|
|
39
|
+
mergeTelemetryContext,
|
|
40
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TelemetryEventBus — the internal event stream telemetry flows onto.
|
|
3
|
+
* Plugins/extensions and the built-in usage rollup
|
|
4
|
+
* subscribe here; it is deliberately independent of OTel export (Decision 7),
|
|
5
|
+
* so usage counters persist even when the OTel exporter is a no-op.
|
|
6
|
+
*
|
|
7
|
+
* This is intentionally a **synchronous** in-process fan-out (Frigg runs one
|
|
8
|
+
* invocation per Lambda container): a plain subscriber list with per-subscriber
|
|
9
|
+
* `try/catch` so a misbehaving subscriber can never break emission or its
|
|
10
|
+
* siblings. No queue/backpressure machinery — emission volume is bounded by a
|
|
11
|
+
* single invocation's work.
|
|
12
|
+
*
|
|
13
|
+
* ## Public event contract (semver-stable)
|
|
14
|
+
* Event types and their payload shapes:
|
|
15
|
+
* - `'metric'` → `{ name: string, value: number, attributes: object, context?: object }`
|
|
16
|
+
* - `'event'` → `{ name: string, attributes: object, context?: object }`
|
|
17
|
+
* `attributes` are the bounded OTel metric labels. `context` (present when an
|
|
18
|
+
* ambient telemetry context is active) carries the high-cardinality identifiers
|
|
19
|
+
* — {integrationId, integrationType, userId, version, ...} plus per-call extras
|
|
20
|
+
* like a request `url` — which the usage rollup reads for attribution. Those ids
|
|
21
|
+
* NEVER appear in `attributes` (Cardinality note).
|
|
22
|
+
*/
|
|
23
|
+
function createTelemetryEventBus() {
|
|
24
|
+
/** @type {Map<string, Set<Function>>} */
|
|
25
|
+
const subscribers = new Map();
|
|
26
|
+
|
|
27
|
+
function on(eventType, callback) {
|
|
28
|
+
if (!subscribers.has(eventType)) {
|
|
29
|
+
subscribers.set(eventType, new Set());
|
|
30
|
+
}
|
|
31
|
+
const set = subscribers.get(eventType);
|
|
32
|
+
set.add(callback);
|
|
33
|
+
return function off() {
|
|
34
|
+
set.delete(callback);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function emit(eventType, payload) {
|
|
39
|
+
const set = subscribers.get(eventType);
|
|
40
|
+
if (!set || set.size === 0) return;
|
|
41
|
+
for (const callback of set) {
|
|
42
|
+
try {
|
|
43
|
+
callback(payload);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// A subscriber must never break emission or its siblings.
|
|
46
|
+
console.warn(
|
|
47
|
+
`[Frigg][telemetry] subscriber for "${eventType}" threw: ${
|
|
48
|
+
error && error.message
|
|
49
|
+
}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { on, emit };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { createTelemetryEventBus };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// The process composition root for telemetry: one shared state, per-part lazy
|
|
2
|
+
// getters, one atomic reset — replacing three separate singleton module caches
|
|
3
|
+
// (telemetry / usage-rollup / plugin-subscribers) that previously had to be
|
|
4
|
+
// reset together in the right order. Bus identity across the parts is guaranteed
|
|
5
|
+
// by construction: the rollup and plugin subscribers attach to the same
|
|
6
|
+
// getTelemetry() instance reached through this module's own closure.
|
|
7
|
+
|
|
8
|
+
const { createTelemetry } = require('./telemetry-service');
|
|
9
|
+
const { NoOpTelemetry } = require('./no-op-telemetry');
|
|
10
|
+
|
|
11
|
+
const UNSET = Symbol('unset');
|
|
12
|
+
|
|
13
|
+
function newState() {
|
|
14
|
+
return { telemetry: null, usageRollup: UNSET, pluginSubscribers: UNSET };
|
|
15
|
+
}
|
|
16
|
+
let state = newState();
|
|
17
|
+
|
|
18
|
+
function loadTelemetryConfig() {
|
|
19
|
+
// Lazy-required to avoid a load-time cycle with the handlers layer.
|
|
20
|
+
const { loadAppDefinition } = require('../handlers/app-definition-loader');
|
|
21
|
+
return loadAppDefinition();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The process telemetry service. Created lazily on first use and reused for the
|
|
26
|
+
* life of the Lambda container, so the OTel SDK initialises at most once per cold
|
|
27
|
+
* start. Never throws — any failure falls back to the no-op.
|
|
28
|
+
*/
|
|
29
|
+
function getTelemetry() {
|
|
30
|
+
if (state.telemetry) return state.telemetry;
|
|
31
|
+
try {
|
|
32
|
+
const { telemetry = {} } = loadTelemetryConfig();
|
|
33
|
+
const stage = process.env.STAGE || process.env.NODE_ENV || 'production';
|
|
34
|
+
state.telemetry = createTelemetry({
|
|
35
|
+
exporter: telemetry.exporter,
|
|
36
|
+
sampleRatio: telemetry.sampleRatio,
|
|
37
|
+
resource: { service: process.env.FRIGG_STACK || 'frigg', stage },
|
|
38
|
+
});
|
|
39
|
+
} catch (_) {
|
|
40
|
+
state.telemetry = new NoOpTelemetry();
|
|
41
|
+
}
|
|
42
|
+
return state.telemetry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The process usage-rollup subscriber, wired to getTelemetry()'s bus. Returns
|
|
47
|
+
* null when usage is disabled (no Definition.usage opt-in and no North Star) or
|
|
48
|
+
* the app definition can't load. Never throws — the warn lets operators tell
|
|
49
|
+
* "usage off" from "usage broken" (e.g. unresolved DB_TYPE).
|
|
50
|
+
*/
|
|
51
|
+
function getUsageRollupSubscriber() {
|
|
52
|
+
if (state.usageRollup !== UNSET) return state.usageRollup;
|
|
53
|
+
state.usageRollup = null;
|
|
54
|
+
try {
|
|
55
|
+
const { computeTrackedMetrics } = require('../usage/tracked-metrics');
|
|
56
|
+
const {
|
|
57
|
+
northStarKeys,
|
|
58
|
+
createNorthStarDerivationSubscriber,
|
|
59
|
+
} = require('./north-star');
|
|
60
|
+
const { integrations = [], telemetry = {} } = loadTelemetryConfig();
|
|
61
|
+
|
|
62
|
+
// Track both Definition.usage opt-ins and any North Star counter keys,
|
|
63
|
+
// so a derived North Star persists even without an explicit usage decl.
|
|
64
|
+
const trackedMetrics = computeTrackedMetrics(integrations);
|
|
65
|
+
for (const key of northStarKeys(telemetry.northStar)) {
|
|
66
|
+
trackedMetrics.add(key);
|
|
67
|
+
}
|
|
68
|
+
if (trackedMetrics.size === 0) return state.usageRollup;
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
createUsageRepository,
|
|
72
|
+
} = require('../usage/repositories/usage-repository-factory');
|
|
73
|
+
const {
|
|
74
|
+
createUsageRollupSubscriber,
|
|
75
|
+
} = require('./usage-rollup-subscriber');
|
|
76
|
+
const telemetryService = getTelemetry(); // same memo → shared bus
|
|
77
|
+
|
|
78
|
+
state.usageRollup = createUsageRollupSubscriber({
|
|
79
|
+
telemetry: telemetryService,
|
|
80
|
+
usageRepository: createUsageRepository(),
|
|
81
|
+
trackedMetrics,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// North Star derived-from-trace: emits the North Star counter when a
|
|
85
|
+
// configured signal matches; the emission flows to the rollup above.
|
|
86
|
+
if (telemetry.northStar) {
|
|
87
|
+
createNorthStarDerivationSubscriber({
|
|
88
|
+
telemetry: telemetryService,
|
|
89
|
+
northStar: telemetry.northStar,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn(
|
|
94
|
+
`[Frigg][usage] rollup disabled: ${error && error.message}`
|
|
95
|
+
);
|
|
96
|
+
state.usageRollup = null;
|
|
97
|
+
}
|
|
98
|
+
return state.usageRollup;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Adopter-declared telemetry subscribers, wired to getTelemetry()'s bus once per
|
|
103
|
+
* cold start. Independent of the usage rollup (adopter taps wire even when no
|
|
104
|
+
* integration declares Definition.usage). Never throws.
|
|
105
|
+
*/
|
|
106
|
+
function getPluginTelemetrySubscribers() {
|
|
107
|
+
if (state.pluginSubscribers !== UNSET) return state.pluginSubscribers;
|
|
108
|
+
state.pluginSubscribers = [];
|
|
109
|
+
try {
|
|
110
|
+
const { wireTelemetrySubscribers } = require('./plugin-subscribers');
|
|
111
|
+
const { telemetry = {} } = loadTelemetryConfig();
|
|
112
|
+
const subscribers = telemetry.subscribers || [];
|
|
113
|
+
if (subscribers.length === 0) return state.pluginSubscribers;
|
|
114
|
+
|
|
115
|
+
state.pluginSubscribers = wireTelemetrySubscribers({
|
|
116
|
+
telemetry: getTelemetry(),
|
|
117
|
+
subscribers,
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`[Frigg][telemetry] plugin subscribers disabled: ${
|
|
122
|
+
error && error.message
|
|
123
|
+
}`
|
|
124
|
+
);
|
|
125
|
+
state.pluginSubscribers = [];
|
|
126
|
+
}
|
|
127
|
+
return state.pluginSubscribers;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Test-only: reset the whole runtime — no stale cross-memo states possible. */
|
|
131
|
+
function resetTelemetryRuntimeForTests() {
|
|
132
|
+
state = newState();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Test-only: install a telemetry service; dependent memos clear atomically. */
|
|
136
|
+
function setTelemetryForTests(telemetry) {
|
|
137
|
+
state = newState();
|
|
138
|
+
state.telemetry = telemetry;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
getTelemetry,
|
|
143
|
+
getUsageRollupSubscriber,
|
|
144
|
+
getPluginTelemetrySubscribers,
|
|
145
|
+
resetTelemetryRuntimeForTests,
|
|
146
|
+
setTelemetryForTests,
|
|
147
|
+
};
|