@powerhousedao/switchboard 6.0.2-staging.5 → 6.0.2-staging.7
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/.env +5 -1
- package/CHANGELOG.md +74 -0
- package/README.md +44 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +125 -41
- package/dist/index.mjs.map +1 -1
- package/dist/install-packages.mjs +4 -1
- package/dist/install-packages.mjs.map +1 -1
- package/dist/migrate.mjs +7 -2
- package/dist/migrate.mjs.map +1 -1
- package/dist/{server-D70Ne9dY.mjs → server-DPxV64Dh.mjs} +101 -22
- package/dist/server-DPxV64Dh.mjs.map +1 -0
- package/dist/server.d.mts +0 -8
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -1
- package/dist/utils-DFl0ezBT.mjs +4 -1
- package/dist/utils-DFl0ezBT.mjs.map +1 -1
- package/dist/utils.mjs +2 -0
- package/package.json +20 -11
- package/test/attachments/routes.test.ts +371 -8
- package/test/pglite-dialect.test.ts +40 -0
- package/dist/server-D70Ne9dY.mjs.map +0 -1
package/.env
CHANGED
|
@@ -5,6 +5,10 @@ PH_SWITCHBOARD_PORT=4001
|
|
|
5
5
|
PH_SWITCHBOARD_API_ORIGIN=http://localhost:3000
|
|
6
6
|
PH_SWITCHBOARD_JWT_SECRET="secret"
|
|
7
7
|
|
|
8
|
+
## Dynamic Model Loading
|
|
9
|
+
PH_REGISTRY_URL=https://registry.dev.vetra.io/
|
|
10
|
+
DYNAMIC_MODEL_LOADING=true
|
|
11
|
+
|
|
8
12
|
## Cache & Storage Options
|
|
9
13
|
PH_SWITCHBOARD_DATABASE_URL="postgresql://postgres:postgres@localhost:5444/postgres"
|
|
10
14
|
PH_SWITCHBOARD_REDIS_URL="redis://localhost:6380"
|
|
@@ -13,4 +17,4 @@ PH_SWITCHBOARD_REDIS_URL="redis://localhost:6380"
|
|
|
13
17
|
SWITCHBOARD_AUTH_ENABLED=true
|
|
14
18
|
SWITCHBOARD_AUTH_GUESTS=""
|
|
15
19
|
SWITCHBOARD_AUTH_USERS=""
|
|
16
|
-
SWITCHBOARD_AUTH_ADMINS="0x1AD3d72e54Fb0eB46e87F82f77B284FC8a66b16C"
|
|
20
|
+
SWITCHBOARD_AUTH_ADMINS="0x1AD3d72e54Fb0eB46e87F82f77B284FC8a66b16C"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
## 6.0.2-staging.7 (2026-05-11)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- **ph-cmd:** added scripts to install dev and staging ph-cmd binary ([492555423](https://github.com/powerhouse-inc/powerhouse/commit/492555423))
|
|
6
|
+
- **switchboard:** bridge OpenTelemetry spans to Sentry ([c1f2fc28b](https://github.com/powerhouse-inc/powerhouse/commit/c1f2fc28b))
|
|
7
|
+
- **ph-cli,ph-cmd,shared:** use lightweight sentry sdk ([248c6b2f6](https://github.com/powerhouse-inc/powerhouse/commit/248c6b2f6))
|
|
8
|
+
- add download button ([#2586](https://github.com/powerhouse-inc/powerhouse/pull/2586))
|
|
9
|
+
- new test-sync-queue cli app that detects sync drift for large drives ([771352e08](https://github.com/powerhouse-inc/powerhouse/commit/771352e08))
|
|
10
|
+
- new test-sync-queue cli app that detects sync drift for large drives ([ddcd53f1e](https://github.com/powerhouse-inc/powerhouse/commit/ddcd53f1e))
|
|
11
|
+
- **connect,reactor-api:** set git hash at build time and display with url ([99b5233c7](https://github.com/powerhouse-inc/powerhouse/commit/99b5233c7))
|
|
12
|
+
- various mixed load scenarios for the lb ([6ef3a76bf](https://github.com/powerhouse-inc/powerhouse/commit/6ef3a76bf))
|
|
13
|
+
- add json viewer for operations tooltip ([#2569](https://github.com/powerhouse-inc/powerhouse/pull/2569))
|
|
14
|
+
- **registry:** renown JWT auth in front of verdaccio ([e5bbf93f1](https://github.com/powerhouse-inc/powerhouse/commit/e5bbf93f1))
|
|
15
|
+
- **switchboard-lb:** rewrite to use simpler drive-id header ([a442207d1](https://github.com/powerhouse-inc/powerhouse/commit/a442207d1))
|
|
16
|
+
- **reactor-attachments:** implementing HEAD, implementing soft-delete and fixing some indexing issues ([f1430bca4](https://github.com/powerhouse-inc/powerhouse/commit/f1430bca4))
|
|
17
|
+
- make document cache usable with graphql client ([#2557](https://github.com/powerhouse-inc/powerhouse/pull/2557))
|
|
18
|
+
|
|
19
|
+
### 🩹 Fixes
|
|
20
|
+
|
|
21
|
+
- **merge:** restore main contents on release branch (keep only version bumps and changelogs) ([6b981a6f1](https://github.com/powerhouse-inc/powerhouse/commit/6b981a6f1))
|
|
22
|
+
- **deps:** drop orphan catalog refs in analytics-engine lost in main merge ([c16fa9085](https://github.com/powerhouse-inc/powerhouse/commit/c16fa9085))
|
|
23
|
+
- **switchboard:** move @pyroscope/nodejs to dependencies ([c71e0b3de](https://github.com/powerhouse-inc/powerhouse/commit/c71e0b3de))
|
|
24
|
+
- **sentry:** inject debug-ids before publish + drop dead dirs ([444c677a2](https://github.com/powerhouse-inc/powerhouse/commit/444c677a2))
|
|
25
|
+
- switching postgres versions ([353951582](https://github.com/powerhouse-inc/powerhouse/commit/353951582))
|
|
26
|
+
- **reactor-api:** exclude hub/spoke test by default, added specific job to test it ([8e8474929](https://github.com/powerhouse-inc/powerhouse/commit/8e8474929))
|
|
27
|
+
- **release:** pass the just-published tag from release -> publish-ph-binaries ([dd19a9b20](https://github.com/powerhouse-inc/powerhouse/commit/dd19a9b20))
|
|
28
|
+
- update dockerfiles for pnpm 11 bin path ([d33db03ce](https://github.com/powerhouse-inc/powerhouse/commit/d33db03ce))
|
|
29
|
+
- **switchboard:** only enable tracing if a destination is configured ([8abff8020](https://github.com/powerhouse-inc/powerhouse/commit/8abff8020))
|
|
30
|
+
- switchboard itself was not using proper env vars ([a18a78f05](https://github.com/powerhouse-inc/powerhouse/commit/a18a78f05))
|
|
31
|
+
- switchboard migrate command did not honor proper env vars ([97f8c4781](https://github.com/powerhouse-inc/powerhouse/commit/97f8c4781))
|
|
32
|
+
- **ci:** pnpm 11 reads PNPM_CONFIG_* not NPM_CONFIG_* ([b6c05fb23](https://github.com/powerhouse-inc/powerhouse/commit/b6c05fb23))
|
|
33
|
+
- **ci, docker:** pnpm 11 uses pnpm-workspace.yaml for allowBuilds; env var for min-release-age ([37c04c28a](https://github.com/powerhouse-inc/powerhouse/commit/37c04c28a))
|
|
34
|
+
- opt out of pnpm 11 minimum-release-age; fix docker/boilerplate strict-dep-builds ([75d31f3c6](https://github.com/powerhouse-inc/powerhouse/commit/75d31f3c6))
|
|
35
|
+
- add pnpm workspace to boilerplate ([1b3a6e78c](https://github.com/powerhouse-inc/powerhouse/commit/1b3a6e78c))
|
|
36
|
+
- **ci:** pnpm v11 docker init + global bin path ([9d93dc20a](https://github.com/powerhouse-inc/powerhouse/commit/9d93dc20a))
|
|
37
|
+
- **reactor-api:** use explicit instrumentation libs ([740931541](https://github.com/powerhouse-inc/powerhouse/commit/740931541))
|
|
38
|
+
- switchboard itself was not using proper env vars ([50a3b842f](https://github.com/powerhouse-inc/powerhouse/commit/50a3b842f))
|
|
39
|
+
- switchboard migrate command did not honor proper env vars ([2fc850209](https://github.com/powerhouse-inc/powerhouse/commit/2fc850209))
|
|
40
|
+
- **eslint:** tighten cold-path rule typing ([dd9e8afcb](https://github.com/powerhouse-inc/powerhouse/commit/dd9e8afcb))
|
|
41
|
+
- update pnpm lock with pnpm 11 ([53489e082](https://github.com/powerhouse-inc/powerhouse/commit/53489e082))
|
|
42
|
+
- bump document-engineering to 1.40.3 and align zod pin ([d50e7e42c](https://github.com/powerhouse-inc/powerhouse/commit/d50e7e42c))
|
|
43
|
+
- **release:** drop concurrency from publish-docker-images.yml ([#2572](https://github.com/powerhouse-inc/powerhouse/issues/2572))
|
|
44
|
+
- **release:** retry git push with rebase + add workflow concurrency ([#2572](https://github.com/powerhouse-inc/powerhouse/pull/2572))
|
|
45
|
+
- **reactor-attachments:** switch to Attachment-Metadata instead of the X- prefix ([7ea3f120a](https://github.com/powerhouse-inc/powerhouse/commit/7ea3f120a))
|
|
46
|
+
- **reactor-api, switchboard:** partial-delete index, reservation validation, fastify param routing, case-insensitive hashes ([f0b5b0620](https://github.com/powerhouse-inc/powerhouse/commit/f0b5b0620))
|
|
47
|
+
- **reactor-attachments:** code-review feedback ([18cd49ab6](https://github.com/powerhouse-inc/powerhouse/commit/18cd49ab6))
|
|
48
|
+
- **codegen,ph-cli,shared:** build package types with tsc ([a1a47e932](https://github.com/powerhouse-inc/powerhouse/commit/a1a47e932))
|
|
49
|
+
- **codegen,ph-cli,shared:** build package types with tsc ([f3658dddc](https://github.com/powerhouse-inc/powerhouse/commit/f3658dddc))
|
|
50
|
+
- **ci:** pick docker tag matching branch channel ([1f6c5ba7c](https://github.com/powerhouse-inc/powerhouse/commit/1f6c5ba7c))
|
|
51
|
+
|
|
52
|
+
### 🔥 Performance
|
|
53
|
+
|
|
54
|
+
- **ph-cmd:** short-circuit --version to skip the heavy clis bundle ([96a37df65](https://github.com/powerhouse-inc/powerhouse/commit/96a37df65))
|
|
55
|
+
|
|
56
|
+
### ❤️ Thank You
|
|
57
|
+
|
|
58
|
+
- acaldas
|
|
59
|
+
- Benjamin Jordan
|
|
60
|
+
- Copilot
|
|
61
|
+
- Frank @froid1911
|
|
62
|
+
- Guillermo Puente @gpuente
|
|
63
|
+
- Ryan Wolhuter @ryanwolhuter
|
|
64
|
+
|
|
65
|
+
## 6.0.2-staging.6 (2026-05-04)
|
|
66
|
+
|
|
67
|
+
### 🩹 Fixes
|
|
68
|
+
|
|
69
|
+
- **reactor-api:** a number of hacks to get around the way vite works, and pre-empt sigterm and sigkill ([36bf0918c](https://github.com/powerhouse-inc/powerhouse/commit/36bf0918c))
|
|
70
|
+
|
|
71
|
+
### ❤️ Thank You
|
|
72
|
+
|
|
73
|
+
- Benjamin Jordan
|
|
74
|
+
|
|
1
75
|
## 6.0.2-staging.5 (2026-05-04)
|
|
2
76
|
|
|
3
77
|
### 🚀 Features
|
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ A powerful document-driven server that provides a unified API for managing and s
|
|
|
13
13
|
- **HTTPS Support**: Built-in HTTPS server with custom certificates
|
|
14
14
|
- **Profiling**: Integration with Pyroscope for performance monitoring
|
|
15
15
|
- **Error Tracking**: Sentry integration for error monitoring and reporting
|
|
16
|
+
- **Observability**: Unified OpenTelemetry tracing + metrics bootstrap, with optional Sentry APM bridging (same trace IDs in Tempo and Sentry)
|
|
16
17
|
|
|
17
18
|
## 📦 Installation
|
|
18
19
|
|
|
@@ -106,11 +107,11 @@ pnpm add -g @powerhousedao/switchboard
|
|
|
106
107
|
| `PH_REACTOR_DATABASE_URL` | PostgreSQL URL (takes precedence) | - |
|
|
107
108
|
| `REDIS_URL` | Redis connection URL | - |
|
|
108
109
|
| `REDIS_TLS_URL` | Redis TLS connection URL | - |
|
|
109
|
-
| `SENTRY_DSN` | Sentry DSN for error tracking | - |
|
|
110
|
-
| `SENTRY_ENV` | Sentry environment | - |
|
|
111
110
|
| `PYROSCOPE_SERVER_ADDRESS` | Pyroscope server address | - |
|
|
112
111
|
| `FEATURE_REACTORV2_ENABLED` | Enable Reactor v2 subgraph feature | `false` |
|
|
113
112
|
|
|
113
|
+
See [Observability](#observability) below for Sentry and OpenTelemetry variables.
|
|
114
|
+
|
|
114
115
|
### Authentication Configuration
|
|
115
116
|
|
|
116
117
|
```typescript
|
|
@@ -132,6 +133,47 @@ Switchboard supports multiple storage backends:
|
|
|
132
133
|
- **PostgreSQL**: Persistent database storage
|
|
133
134
|
- **Redis**: Caching layer (optional)
|
|
134
135
|
|
|
136
|
+
### Observability
|
|
137
|
+
|
|
138
|
+
Switchboard bootstraps Sentry and OpenTelemetry from a single module (`src/observability.mts`) that is imported as the very first thing in `src/index.mts`. The OpenTelemetry instrumentations (`http`, `express`, `pg`, `graphql`) register require-time hooks at load, so the import order must not be changed.
|
|
139
|
+
|
|
140
|
+
What runs depends on which environment variables are set:
|
|
141
|
+
|
|
142
|
+
- `SENTRY_DSN` set → Sentry error reporting is initialized.
|
|
143
|
+
- `ENABLE_TRACING=true` or `NODE_ENV=production`, **and** at least one trace destination is configured (`TEMPO_ENDPOINT` or `SENTRY_DSN`) → OpenTelemetry tracing is initialized. Spans go to Tempo over OTLP HTTP if `TEMPO_ENDPOINT` is set, and/or to Sentry via `SentrySpanProcessor` if `SENTRY_DSN` is set. When both are set, the same trace IDs appear in Tempo and Sentry and cross-link in Grafana.
|
|
144
|
+
- Tracing requested (prod or `ENABLE_TRACING=true`) but no destination configured → a warning is logged and tracing is **not** started. This avoids the cost of patching `http`/`express`/`pg`/`graphql` and generating spans only to drop them. Set `TEMPO_ENDPOINT` and/or `SENTRY_DSN` to enable.
|
|
145
|
+
- `OTEL_EXPORTER_OTLP_ENDPOINT` set → metrics export to that OTLP HTTP endpoint via a periodic reader. Reactor metrics emitted by `@powerhousedao/opentelemetry-instrumentation-reactor` flow through the same global meter provider.
|
|
146
|
+
|
|
147
|
+
If neither Sentry nor a tracing destination is configured, the module is a no-op and no exporters or instrumentations are registered.
|
|
148
|
+
|
|
149
|
+
#### Environment Variables
|
|
150
|
+
|
|
151
|
+
| Variable | Description | Default |
|
|
152
|
+
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
|
153
|
+
| `SENTRY_DSN` | Sentry DSN. When unset, Sentry is disabled. | - |
|
|
154
|
+
| `SENTRY_ENV` | `environment` tag passed to `Sentry.init`. | - |
|
|
155
|
+
| `SENTRY_RELEASE` | Release tag (must match the version uploaded by CI for source maps to resolve). | `v${npm_package_version}` if available |
|
|
156
|
+
| `SENTRY_TRACES_SAMPLE_RATE` | APM sampling rate (0.0–1.0). | `0.1` |
|
|
157
|
+
| `ENABLE_TRACING` | Set to `true` to request tracing outside production. Tracing only starts when a destination is also set. | `false` (also requested when `NODE_ENV=production`) |
|
|
158
|
+
| `NODE_ENV` | When `production`, tracing is requested automatically. Also exported as `deployment.environment` attribute. | `development` |
|
|
159
|
+
| `OTEL_SERVICE_NAME` | `service.name` resource attribute. | `switchboard` |
|
|
160
|
+
| `TENANT_ID` | `tenant.id` resource attribute (used to slice traces by tenant in Grafana). | `default` |
|
|
161
|
+
| `TEMPO_ENDPOINT` | OTLP HTTP endpoint for trace export. When unset, OTLP trace export is disabled. In-cluster deploys typically set `http://tempo.monitoring.svc.cluster.local:4318/v1/traces`. | - |
|
|
162
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP HTTP endpoint for metrics export. Metrics export is disabled when unset. | - |
|
|
163
|
+
| `OTEL_METRIC_EXPORT_INTERVAL` | Metric export interval in milliseconds. | `60000` |
|
|
164
|
+
|
|
165
|
+
#### Local Development
|
|
166
|
+
|
|
167
|
+
Tracing is off by default outside production, so a bare `pnpm dev` does not need any of these set. To exercise the full pipeline locally, point `TEMPO_ENDPOINT` and `OTEL_EXPORTER_OTLP_ENDPOINT` at a local OTel collector (or Tempo + Prometheus) and run with `ENABLE_TRACING=true`:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
ENABLE_TRACING=true \
|
|
171
|
+
TEMPO_ENDPOINT=http://localhost:4318/v1/traces \
|
|
172
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/metrics \
|
|
173
|
+
SENTRY_DSN=... \
|
|
174
|
+
pnpm dev
|
|
175
|
+
```
|
|
176
|
+
|
|
135
177
|
## 🐳 Docker Deployment
|
|
136
178
|
|
|
137
179
|
### Using Docker Compose
|
package/dist/index.d.mts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
2
|
+
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5ffe851c-e77f-513a-a99b-e5f1ba344644")}catch(e){}}();
|
|
4
|
+
import { n as startSwitchboard, r as parseForcePgVersion } from "./server-DPxV64Dh.mjs";
|
|
3
5
|
import "./utils-DFl0ezBT.mjs";
|
|
6
|
+
import { metrics } from "@opentelemetry/api";
|
|
7
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
8
|
+
import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
|
|
9
|
+
import { GraphQLInstrumentation } from "@opentelemetry/instrumentation-graphql";
|
|
10
|
+
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
|
11
|
+
import { PgInstrumentation } from "@opentelemetry/instrumentation-pg";
|
|
12
|
+
import { Resource } from "@opentelemetry/resources";
|
|
13
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
14
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
15
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
4
16
|
import * as Sentry from "@sentry/node";
|
|
17
|
+
import { SentryPropagator, SentrySpanProcessor } from "@sentry/opentelemetry";
|
|
5
18
|
import { childLogger } from "document-model";
|
|
6
|
-
import dotenv from "dotenv";
|
|
7
|
-
import { getConfig } from "@powerhousedao/config/node";
|
|
8
19
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
9
|
-
import { Resource } from "@opentelemetry/resources";
|
|
10
20
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
21
|
+
import dotenv from "dotenv";
|
|
22
|
+
import { getConfig } from "@powerhousedao/config/node";
|
|
23
|
+
//#region src/metrics.ts
|
|
24
|
+
const logger$2 = childLogger(["switchboard", "metrics"]);
|
|
25
|
+
function createMeterProviderFromEnv(env) {
|
|
26
|
+
const endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
27
|
+
if (!endpoint) return void 0;
|
|
28
|
+
const parsed = parseInt(env.OTEL_METRIC_EXPORT_INTERVAL ?? "", 10);
|
|
29
|
+
const exportIntervalMillis = Number.isFinite(parsed) && parsed > 0 ? parsed : 5e3;
|
|
30
|
+
const base = endpoint.replace(/\/$/, "");
|
|
31
|
+
const exporterUrl = base.endsWith("/v1/metrics") ? base : `${base}/v1/metrics`;
|
|
32
|
+
logger$2.info(`Initializing OpenTelemetry metrics exporter at: ${endpoint}`);
|
|
33
|
+
const meterProvider = new MeterProvider({
|
|
34
|
+
resource: new Resource({ "service.name": env.OTEL_SERVICE_NAME ?? "switchboard" }),
|
|
35
|
+
readers: [new PeriodicExportingMetricReader({
|
|
36
|
+
exporter: new OTLPMetricExporter({ url: exporterUrl }),
|
|
37
|
+
exportIntervalMillis,
|
|
38
|
+
exportTimeoutMillis: Math.max(exportIntervalMillis - 250, 1)
|
|
39
|
+
})]
|
|
40
|
+
});
|
|
41
|
+
logger$2.info(`Metrics export enabled (interval: ${exportIntervalMillis}ms)`);
|
|
42
|
+
return meterProvider;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/observability.mts
|
|
46
|
+
const logger$1 = childLogger(["switchboard", "observability"]);
|
|
47
|
+
const SERVICE_NAME = process.env.OTEL_SERVICE_NAME || "switchboard";
|
|
48
|
+
const SERVICE_VERSION = process.env.npm_package_version || "unknown";
|
|
49
|
+
const TENANT_ID = process.env.TENANT_ID || "default";
|
|
50
|
+
const DEPLOY_ENV = process.env.NODE_ENV || "development";
|
|
51
|
+
const TEMPO_ENDPOINT = process.env.TEMPO_ENDPOINT;
|
|
52
|
+
const SENTRY_DSN = process.env.SENTRY_DSN;
|
|
53
|
+
const TRACING_REQUESTED = process.env.ENABLE_TRACING === "true" || process.env.NODE_ENV === "production";
|
|
54
|
+
const HAS_TRACE_DESTINATION = Boolean(TEMPO_ENDPOINT) || Boolean(SENTRY_DSN);
|
|
55
|
+
const TRACING_ENABLED = TRACING_REQUESTED && HAS_TRACE_DESTINATION;
|
|
56
|
+
if (TRACING_REQUESTED && !HAS_TRACE_DESTINATION) logger$1.warn("Tracing was requested (NODE_ENV=production or ENABLE_TRACING=true) but no destination is configured — instrumentation will not run. Set TEMPO_ENDPOINT (e.g. http://tempo.monitoring.svc.cluster.local:4318/v1/traces) to export OTLP spans, and/or SENTRY_DSN to forward spans to Sentry.");
|
|
57
|
+
const SENTRY_TRACES_SAMPLE_RATE = parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE ?? "0.1");
|
|
58
|
+
if (SENTRY_DSN) {
|
|
59
|
+
logger$1.info("Initialized Sentry with env: @env", process.env.SENTRY_ENV);
|
|
60
|
+
Sentry.init({
|
|
61
|
+
dsn: SENTRY_DSN,
|
|
62
|
+
environment: process.env.SENTRY_ENV,
|
|
63
|
+
release: process.env.SENTRY_RELEASE || (process.env.npm_package_version ? `v${process.env.npm_package_version}` : void 0),
|
|
64
|
+
tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE,
|
|
65
|
+
skipOpenTelemetrySetup: TRACING_ENABLED
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const meterProvider = createMeterProviderFromEnv({
|
|
69
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
70
|
+
OTEL_METRIC_EXPORT_INTERVAL: process.env.OTEL_METRIC_EXPORT_INTERVAL,
|
|
71
|
+
OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME
|
|
72
|
+
});
|
|
73
|
+
if (meterProvider) metrics.setGlobalMeterProvider(meterProvider);
|
|
74
|
+
let sdk;
|
|
75
|
+
if (TRACING_ENABLED) {
|
|
76
|
+
logger$1.info(`Initializing OpenTelemetry tracing for ${SERVICE_NAME}`);
|
|
77
|
+
if (TEMPO_ENDPOINT) logger$1.info(` Tempo endpoint: ${TEMPO_ENDPOINT}`);
|
|
78
|
+
if (SENTRY_DSN) logger$1.info(` Sentry span forwarding: enabled`);
|
|
79
|
+
logger$1.info(` Tenant: ${TENANT_ID}`);
|
|
80
|
+
const resource = new Resource({
|
|
81
|
+
[ATTR_SERVICE_NAME]: SERVICE_NAME,
|
|
82
|
+
[ATTR_SERVICE_VERSION]: SERVICE_VERSION,
|
|
83
|
+
"tenant.id": TENANT_ID,
|
|
84
|
+
"deployment.environment": DEPLOY_ENV
|
|
85
|
+
});
|
|
86
|
+
const spanProcessors = [];
|
|
87
|
+
if (TEMPO_ENDPOINT) spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({ url: TEMPO_ENDPOINT })));
|
|
88
|
+
if (SENTRY_DSN) spanProcessors.push(new SentrySpanProcessor());
|
|
89
|
+
sdk = new NodeSDK({
|
|
90
|
+
resource,
|
|
91
|
+
spanProcessors,
|
|
92
|
+
textMapPropagator: SENTRY_DSN ? new SentryPropagator() : void 0,
|
|
93
|
+
instrumentations: [
|
|
94
|
+
new HttpInstrumentation({
|
|
95
|
+
ignoreIncomingRequestHook: (req) => req.url === "/health" || req.url === "/ready",
|
|
96
|
+
requireParentforIncomingSpans: false,
|
|
97
|
+
requireParentforOutgoingSpans: false,
|
|
98
|
+
requestHook: (span, request) => {
|
|
99
|
+
span.setAttribute("http.route", request.url || "");
|
|
100
|
+
},
|
|
101
|
+
responseHook: (span, response) => {
|
|
102
|
+
if (response.statusCode) span.setAttribute("http.status_code", response.statusCode);
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
105
|
+
new ExpressInstrumentation({ requestHook: (span, info) => {
|
|
106
|
+
if (info.route) span.setAttribute("http.route", info.route);
|
|
107
|
+
} }),
|
|
108
|
+
new GraphQLInstrumentation({
|
|
109
|
+
mergeItems: true,
|
|
110
|
+
allowValues: true
|
|
111
|
+
}),
|
|
112
|
+
new PgInstrumentation({ enhancedDatabaseReporting: true })
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
sdk.start();
|
|
116
|
+
if (SENTRY_DSN && typeof Sentry.validateOpenTelemetrySetup === "function") Sentry.validateOpenTelemetrySetup();
|
|
117
|
+
logger$1.info("OpenTelemetry tracing initialized");
|
|
118
|
+
}
|
|
119
|
+
async function shutdown() {
|
|
120
|
+
await Promise.race([Promise.all([meterProvider?.shutdown().catch(() => void 0), sdk?.shutdown().catch(() => void 0)]), new Promise((resolve) => setTimeout(resolve, 5e3))]);
|
|
121
|
+
}
|
|
122
|
+
process.on("SIGINT", () => {
|
|
123
|
+
shutdown().finally(() => process.exit(0));
|
|
124
|
+
});
|
|
125
|
+
process.on("SIGTERM", () => {
|
|
126
|
+
shutdown().finally(() => process.exit(0));
|
|
127
|
+
});
|
|
128
|
+
//#endregion
|
|
11
129
|
//#region src/config.ts
|
|
12
130
|
dotenv.config();
|
|
13
131
|
const { switchboard } = getConfig();
|
|
@@ -33,28 +151,6 @@ const config = {
|
|
|
33
151
|
}
|
|
34
152
|
};
|
|
35
153
|
//#endregion
|
|
36
|
-
//#region src/metrics.ts
|
|
37
|
-
const logger$1 = childLogger(["switchboard", "metrics"]);
|
|
38
|
-
function createMeterProviderFromEnv(env) {
|
|
39
|
-
const endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
40
|
-
if (!endpoint) return void 0;
|
|
41
|
-
const parsed = parseInt(env.OTEL_METRIC_EXPORT_INTERVAL ?? "", 10);
|
|
42
|
-
const exportIntervalMillis = Number.isFinite(parsed) && parsed > 0 ? parsed : 5e3;
|
|
43
|
-
const base = endpoint.replace(/\/$/, "");
|
|
44
|
-
const exporterUrl = base.endsWith("/v1/metrics") ? base : `${base}/v1/metrics`;
|
|
45
|
-
logger$1.info(`Initializing OpenTelemetry metrics exporter at: ${endpoint}`);
|
|
46
|
-
const meterProvider = new MeterProvider({
|
|
47
|
-
resource: new Resource({ "service.name": env.OTEL_SERVICE_NAME ?? "switchboard" }),
|
|
48
|
-
readers: [new PeriodicExportingMetricReader({
|
|
49
|
-
exporter: new OTLPMetricExporter({ url: exporterUrl }),
|
|
50
|
-
exportIntervalMillis,
|
|
51
|
-
exportTimeoutMillis: Math.max(exportIntervalMillis - 250, 1)
|
|
52
|
-
})]
|
|
53
|
-
});
|
|
54
|
-
logger$1.info(`Metrics export enabled (interval: ${exportIntervalMillis}ms)`);
|
|
55
|
-
return meterProvider;
|
|
56
|
-
}
|
|
57
|
-
//#endregion
|
|
58
154
|
//#region src/profiler.ts
|
|
59
155
|
async function initProfilerFromEnv(env) {
|
|
60
156
|
const { PYROSCOPE_SERVER_ADDRESS: serverAddress, PYROSCOPE_APPLICATION_NAME: appName, PYROSCOPE_USER: basicAuthUser, PYROSCOPE_PASSWORD: basicAuthPassword, PYROSCOPE_WALL_ENABLED: wallEnabled, PYROSCOPE_HEAP_ENABLED: heapEnabled } = env;
|
|
@@ -103,18 +199,6 @@ function ensureNodeVersion(minVersion = "24") {
|
|
|
103
199
|
}
|
|
104
200
|
ensureNodeVersion("24");
|
|
105
201
|
process.setMaxListeners(0);
|
|
106
|
-
const meterProvider = createMeterProviderFromEnv({
|
|
107
|
-
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
108
|
-
OTEL_METRIC_EXPORT_INTERVAL: process.env.OTEL_METRIC_EXPORT_INTERVAL,
|
|
109
|
-
OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME
|
|
110
|
-
});
|
|
111
|
-
async function shutdown() {
|
|
112
|
-
console.log("\nShutting down...");
|
|
113
|
-
await Promise.race([meterProvider?.shutdown().catch(() => void 0), new Promise((resolve) => setTimeout(resolve, 5e3))]);
|
|
114
|
-
process.exit(0);
|
|
115
|
-
}
|
|
116
|
-
process.on("SIGINT", shutdown);
|
|
117
|
-
process.on("SIGTERM", shutdown);
|
|
118
202
|
if (process.env.PYROSCOPE_SERVER_ADDRESS) try {
|
|
119
203
|
await initProfilerFromEnv(process.env);
|
|
120
204
|
} catch (e) {
|
|
@@ -125,10 +209,10 @@ const cliMigratePglite = process.argv.slice(2).includes("--migrate-pglite");
|
|
|
125
209
|
startSwitchboard({
|
|
126
210
|
...config,
|
|
127
211
|
migratePglite: cliMigratePglite || config.migratePglite,
|
|
128
|
-
forcePgVersion: config.forcePgVersion ?? void 0
|
|
129
|
-
meterProvider
|
|
212
|
+
forcePgVersion: config.forcePgVersion ?? void 0
|
|
130
213
|
}).catch(console.error);
|
|
131
214
|
//#endregion
|
|
132
215
|
export {};
|
|
133
216
|
|
|
134
|
-
//# sourceMappingURL=index.mjs.map
|
|
217
|
+
//# sourceMappingURL=index.mjs.map
|
|
218
|
+
//# debugId=5ffe851c-e77f-513a-a99b-e5f1ba344644
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["logger"],"sources":["../src/config.ts","../src/metrics.ts","../src/profiler.ts","../src/index.mts"],"sourcesContent":["import dotenv from \"dotenv\";\ndotenv.config();\n\nimport { getConfig } from \"@powerhousedao/config/node\";\nimport type { DriveInput } from \"@powerhousedao/shared/document-drive\";\nimport { parseForcePgVersion } from \"./pglite-version.js\";\nconst phConfig = getConfig();\nconst { switchboard } = phConfig;\ninterface Config {\n database: {\n url: string;\n };\n port: number;\n mcp: boolean;\n migratePglite: boolean;\n forcePgVersion: 16 | 17 | null;\n drive: DriveInput;\n}\nexport const config: Config = {\n database: {\n // url: process.env.PH_SWITCHBOARD_DATABASE_URL ?? switchboard?.database?.url ?? \"dev.db\",\n url:\n process.env.PH_SWITCHBOARD_DATABASE_URL ??\n switchboard?.database?.url ??\n \"dev.db\",\n },\n port:\n process.env.PH_SWITCHBOARD_PORT &&\n !isNaN(Number(process.env.PH_SWITCHBOARD_PORT))\n ? Number(process.env.PH_SWITCHBOARD_PORT)\n : (switchboard?.port ?? 4001),\n mcp: true,\n migratePglite: process.env.PH_MIGRATE_PGLITE === \"true\",\n forcePgVersion: parseForcePgVersion(process.env.PH_FORCE_PG_VERSION),\n drive: {\n id: \"powerhouse\",\n slug: \"powerhouse\",\n global: {\n name: \"Powerhouse\",\n icon: \"https://ipfs.io/ipfs/QmcaTDBYn8X2psGaXe7iQ6qd8q6oqHLgxvMX9yXf7f9uP7\",\n },\n local: {\n availableOffline: true,\n listeners: [],\n sharingType: \"public\",\n triggers: [],\n },\n },\n};\n","import { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { Resource } from \"@opentelemetry/resources\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport { childLogger } from \"document-model\";\n\nconst logger = childLogger([\"switchboard\", \"metrics\"]);\n\nexport function createMeterProviderFromEnv(env: {\n OTEL_EXPORTER_OTLP_ENDPOINT?: string;\n OTEL_METRIC_EXPORT_INTERVAL?: string;\n OTEL_SERVICE_NAME?: string;\n}): MeterProvider | undefined {\n const endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;\n if (!endpoint) return undefined;\n\n const parsed = parseInt(env.OTEL_METRIC_EXPORT_INTERVAL ?? \"\", 10);\n const exportIntervalMillis =\n Number.isFinite(parsed) && parsed > 0 ? parsed : 5_000;\n\n const base = endpoint.replace(/\\/$/, \"\");\n const exporterUrl = base.endsWith(\"/v1/metrics\")\n ? base\n : `${base}/v1/metrics`;\n\n logger.info(`Initializing OpenTelemetry metrics exporter at: ${endpoint}`);\n const meterProvider = new MeterProvider({\n resource: new Resource({\n \"service.name\": env.OTEL_SERVICE_NAME ?? \"switchboard\",\n }),\n readers: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: exporterUrl,\n }),\n exportIntervalMillis,\n exportTimeoutMillis: Math.max(exportIntervalMillis - 250, 1),\n }),\n ],\n });\n logger.info(`Metrics export enabled (interval: ${exportIntervalMillis}ms)`);\n return meterProvider;\n}\n","import type { PyroscopeConfig } from \"@pyroscope/nodejs\";\n\nexport async function initProfilerFromEnv(env: typeof process.env) {\n const {\n PYROSCOPE_SERVER_ADDRESS: serverAddress,\n PYROSCOPE_APPLICATION_NAME: appName,\n PYROSCOPE_USER: basicAuthUser,\n PYROSCOPE_PASSWORD: basicAuthPassword,\n PYROSCOPE_WALL_ENABLED: wallEnabled,\n PYROSCOPE_HEAP_ENABLED: heapEnabled,\n } = env;\n\n const options: PyroscopeConfig = {\n serverAddress,\n appName,\n basicAuthUser,\n basicAuthPassword,\n // Wall profiling captures wall-clock time (includes async I/O waits)\n // This shows GraphQL resolvers even when waiting for database\n wall: {\n samplingDurationMs: 10000, // 10 second sampling windows\n samplingIntervalMicros: 10000, // 10ms sampling interval (100 samples/sec)\n collectCpuTime: true, // Also collect CPU time alongside wall time\n },\n // Heap profiling for memory allocation tracking\n heap: {\n samplingIntervalBytes: 512 * 1024, // Sample every 512KB allocated\n stackDepth: 64, // Capture deeper stacks for better context\n },\n };\n return initProfiler(options, {\n wallEnabled: wallEnabled !== \"false\",\n heapEnabled: heapEnabled === \"true\",\n });\n}\n\ninterface ProfilerFlags {\n wallEnabled?: boolean;\n heapEnabled?: boolean;\n}\n\nexport async function initProfiler(\n options?: PyroscopeConfig,\n flags: ProfilerFlags = { wallEnabled: true, heapEnabled: false },\n) {\n console.log(\"Initializing Pyroscope profiler at:\", options?.serverAddress);\n console.log(\" Wall profiling:\", flags.wallEnabled ? \"enabled\" : \"disabled\");\n console.log(\" Heap profiling:\", flags.heapEnabled ? \"enabled\" : \"disabled\");\n\n const { default: Pyroscope } = await import(\"@pyroscope/nodejs\");\n Pyroscope.init(options);\n\n // Start wall profiling (captures async I/O time - shows resolvers)\n if (flags.wallEnabled) {\n Pyroscope.startWallProfiling();\n }\n\n // Start CPU profiling (captures CPU-bound work)\n Pyroscope.startCpuProfiling();\n\n // Optionally start heap profiling (memory allocations)\n if (flags.heapEnabled) {\n Pyroscope.startHeapProfiling();\n }\n}\n","#!/usr/bin/env node\nimport * as Sentry from \"@sentry/node\";\nimport { childLogger } from \"document-model\";\nimport { config } from \"./config.js\";\nimport { createMeterProviderFromEnv } from \"./metrics.js\";\nimport { initProfilerFromEnv } from \"./profiler.js\";\nimport { startSwitchboard } from \"./server.mjs\";\n\nconst logger = childLogger([\"switchboard\"]);\n\nfunction ensureNodeVersion(minVersion = \"24\") {\n const version = process.versions.node;\n if (!version) {\n return;\n }\n\n if (version < minVersion) {\n console.error(\n `Node version ${minVersion} or higher is required. Current version: ${version}`,\n );\n process.exit(1);\n }\n}\n// Ensure minimum Node.js version\nensureNodeVersion(\"24\");\n\n// Each subgraph registers its own SIGINT/SIGTERM listeners, and the count\n// scales with dynamically-loaded document models beyond the default cap of 10.\nprocess.setMaxListeners(0);\n\nconst meterProvider = createMeterProviderFromEnv({\n OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,\n OTEL_METRIC_EXPORT_INTERVAL: process.env.OTEL_METRIC_EXPORT_INTERVAL,\n OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME,\n});\n\nasync function shutdown() {\n console.log(\"\\nShutting down...\");\n // Flush final metrics before exit. Races against a 5s deadline so an\n // unresponsive OTLP endpoint cannot exhaust terminationGracePeriodSeconds.\n await Promise.race([\n meterProvider?.shutdown().catch(() => undefined),\n new Promise<void>((resolve) => setTimeout(resolve, 5_000)),\n ]);\n process.exit(0);\n}\n\n// SIGINT: Ctrl-C in development; SIGTERM: graceful shutdown in Docker/Kubernetes\nprocess.on(\"SIGINT\", shutdown);\nprocess.on(\"SIGTERM\", shutdown);\n\nif (process.env.PYROSCOPE_SERVER_ADDRESS) {\n try {\n await initProfilerFromEnv(process.env);\n } catch (e) {\n Sentry.captureException(e);\n logger.error(\"Error starting profiler: @error\", e);\n }\n}\n\nconst cliMigratePglite = process.argv.slice(2).includes(\"--migrate-pglite\");\n\nstartSwitchboard({\n ...config,\n migratePglite: cliMigratePglite || config.migratePglite,\n forcePgVersion: config.forcePgVersion ?? undefined,\n meterProvider,\n}).catch(console.error);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/metrics.ts","../src/observability.mts","../src/config.ts","../src/profiler.ts","../src/index.mts"],"sourcesContent":["import { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { Resource } from \"@opentelemetry/resources\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport { childLogger } from \"document-model\";\n\nconst logger = childLogger([\"switchboard\", \"metrics\"]);\n\nexport function createMeterProviderFromEnv(env: {\n OTEL_EXPORTER_OTLP_ENDPOINT?: string;\n OTEL_METRIC_EXPORT_INTERVAL?: string;\n OTEL_SERVICE_NAME?: string;\n}): MeterProvider | undefined {\n const endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;\n if (!endpoint) return undefined;\n\n const parsed = parseInt(env.OTEL_METRIC_EXPORT_INTERVAL ?? \"\", 10);\n const exportIntervalMillis =\n Number.isFinite(parsed) && parsed > 0 ? parsed : 5_000;\n\n const base = endpoint.replace(/\\/$/, \"\");\n const exporterUrl = base.endsWith(\"/v1/metrics\")\n ? base\n : `${base}/v1/metrics`;\n\n logger.info(`Initializing OpenTelemetry metrics exporter at: ${endpoint}`);\n const meterProvider = new MeterProvider({\n resource: new Resource({\n \"service.name\": env.OTEL_SERVICE_NAME ?? \"switchboard\",\n }),\n readers: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: exporterUrl,\n }),\n exportIntervalMillis,\n exportTimeoutMillis: Math.max(exportIntervalMillis - 250, 1),\n }),\n ],\n });\n logger.info(`Metrics export enabled (interval: ${exportIntervalMillis}ms)`);\n return meterProvider;\n}\n","// Single observability bootstrap: Sentry + OpenTelemetry (tracing + metrics).\n//\n// MUST be imported as the very first module in apps/switchboard/src/index.mts.\n// OpenTelemetry instrumentations register require-time hooks at module load,\n// so http/express/pg/graphql must not be imported (transitively) before this\n// file runs.\n//\n// Replaces three legacy bootstrap sites:\n// - apps/switchboard/src/server.mts top-level Sentry.init\n// - apps/switchboard/src/metrics.ts standalone MeterProvider (still exported\n// here via createMeterProviderFromEnv so its tests keep passing)\n// - packages/reactor-api/src/tracing.ts side-effect NodeSDK\nimport { metrics } from \"@opentelemetry/api\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { ExpressInstrumentation } from \"@opentelemetry/instrumentation-express\";\nimport { GraphQLInstrumentation } from \"@opentelemetry/instrumentation-graphql\";\nimport { HttpInstrumentation } from \"@opentelemetry/instrumentation-http\";\nimport { PgInstrumentation } from \"@opentelemetry/instrumentation-pg\";\nimport { Resource } from \"@opentelemetry/resources\";\nimport type { MeterProvider } from \"@opentelemetry/sdk-metrics\";\nimport { NodeSDK } from \"@opentelemetry/sdk-node\";\nimport {\n BatchSpanProcessor,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\nimport * as Sentry from \"@sentry/node\";\nimport { SentryPropagator, SentrySpanProcessor } from \"@sentry/opentelemetry\";\nimport { childLogger } from \"document-model\";\nimport type { IncomingMessage } from \"node:http\";\nimport { createMeterProviderFromEnv } from \"./metrics.js\";\n\nconst logger = childLogger([\"switchboard\", \"observability\"]);\n\nconst SERVICE_NAME = process.env.OTEL_SERVICE_NAME || \"switchboard\";\nconst SERVICE_VERSION = process.env.npm_package_version || \"unknown\";\nconst TENANT_ID = process.env.TENANT_ID || \"default\";\nconst DEPLOY_ENV = process.env.NODE_ENV || \"development\";\n\nconst TEMPO_ENDPOINT = process.env.TEMPO_ENDPOINT;\nconst SENTRY_DSN = process.env.SENTRY_DSN;\n\nconst TRACING_REQUESTED =\n process.env.ENABLE_TRACING === \"true\" ||\n process.env.NODE_ENV === \"production\";\nconst HAS_TRACE_DESTINATION = Boolean(TEMPO_ENDPOINT) || Boolean(SENTRY_DSN);\nconst TRACING_ENABLED = TRACING_REQUESTED && HAS_TRACE_DESTINATION;\n\nif (TRACING_REQUESTED && !HAS_TRACE_DESTINATION) {\n logger.warn(\n \"Tracing was requested (NODE_ENV=production or ENABLE_TRACING=true) but \" +\n \"no destination is configured — instrumentation will not run. Set \" +\n \"TEMPO_ENDPOINT (e.g. http://tempo.monitoring.svc.cluster.local:4318/v1/traces) \" +\n \"to export OTLP spans, and/or SENTRY_DSN to forward spans to Sentry.\",\n );\n}\n\n// Default 10% APM sampling — Sentry's own production guidance; overridable\n// per-deploy. Only kicks in once tracesSampleRate * (sampler decision) lands.\nconst SENTRY_TRACES_SAMPLE_RATE = parseFloat(\n process.env.SENTRY_TRACES_SAMPLE_RATE ?? \"0.1\",\n);\n\nif (SENTRY_DSN) {\n logger.info(\"Initialized Sentry with env: @env\", process.env.SENTRY_ENV);\n Sentry.init({\n dsn: SENTRY_DSN,\n environment: process.env.SENTRY_ENV,\n // Match the version tag uploaded by release-branch.yml so source maps\n // resolve. Populated by the CI (WORKSPACE_VERSION) or npm at runtime.\n release:\n process.env.SENTRY_RELEASE ||\n (process.env.npm_package_version\n ? `v${process.env.npm_package_version}`\n : undefined),\n tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE,\n // When tracing is on, our NodeSDK below owns the OTel globals and Sentry\n // receives spans via SentrySpanProcessor. Skipping Sentry's bundled OTel\n // setup avoids two TracerProviders fighting over setGlobalTracerProvider.\n // When tracing is off, leave the flag unset so @sentry/node's default\n // auto-OTel still records HTTP transactions for the APM dashboard.\n skipOpenTelemetrySetup: TRACING_ENABLED,\n });\n}\n\nconst meterProvider: MeterProvider | undefined = createMeterProviderFromEnv({\n OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,\n OTEL_METRIC_EXPORT_INTERVAL: process.env.OTEL_METRIC_EXPORT_INTERVAL,\n OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME,\n});\nif (meterProvider) {\n // One-way door: must register before any code calls metrics.getMeter() —\n // most notably ReactorInstrumentation inside the reactor module.\n metrics.setGlobalMeterProvider(meterProvider);\n}\n\nlet sdk: NodeSDK | undefined;\n\nif (TRACING_ENABLED) {\n logger.info(`Initializing OpenTelemetry tracing for ${SERVICE_NAME}`);\n if (TEMPO_ENDPOINT) logger.info(` Tempo endpoint: ${TEMPO_ENDPOINT}`);\n if (SENTRY_DSN) logger.info(` Sentry span forwarding: enabled`);\n logger.info(` Tenant: ${TENANT_ID}`);\n\n const resource = new Resource({\n [ATTR_SERVICE_NAME]: SERVICE_NAME,\n [ATTR_SERVICE_VERSION]: SERVICE_VERSION,\n \"tenant.id\": TENANT_ID,\n \"deployment.environment\": DEPLOY_ENV,\n });\n\n const spanProcessors: SpanProcessor[] = [];\n if (TEMPO_ENDPOINT) {\n spanProcessors.push(\n new BatchSpanProcessor(new OTLPTraceExporter({ url: TEMPO_ENDPOINT })),\n );\n }\n if (SENTRY_DSN) {\n // Fan the same OTel spans into Sentry — same trace IDs as Tempo, so\n // Sentry transactions cross-link to traces in Grafana.\n spanProcessors.push(new SentrySpanProcessor());\n }\n\n sdk = new NodeSDK({\n resource,\n spanProcessors,\n textMapPropagator: SENTRY_DSN ? new SentryPropagator() : undefined,\n instrumentations: [\n new HttpInstrumentation({\n ignoreIncomingRequestHook: (req) =>\n req.url === \"/health\" || req.url === \"/ready\",\n requireParentforIncomingSpans: false,\n requireParentforOutgoingSpans: false,\n requestHook: (span, request) => {\n span.setAttribute(\n \"http.route\",\n (request as IncomingMessage).url || \"\",\n );\n },\n responseHook: (span, response) => {\n if (response.statusCode) {\n span.setAttribute(\"http.status_code\", response.statusCode);\n }\n },\n }),\n new ExpressInstrumentation({\n requestHook: (span, info) => {\n if (info.route) span.setAttribute(\"http.route\", info.route);\n },\n }),\n new GraphQLInstrumentation({ mergeItems: true, allowValues: true }),\n new PgInstrumentation({ enhancedDatabaseReporting: true }),\n ],\n });\n sdk.start();\n if (SENTRY_DSN && typeof Sentry.validateOpenTelemetrySetup === \"function\") {\n Sentry.validateOpenTelemetrySetup();\n }\n logger.info(\"OpenTelemetry tracing initialized\");\n}\n\nasync function shutdown() {\n await Promise.race([\n Promise.all([\n meterProvider?.shutdown().catch(() => undefined),\n sdk?.shutdown().catch(() => undefined),\n ]),\n new Promise<void>((resolve) => setTimeout(resolve, 5_000)),\n ]);\n}\n\nprocess.on(\"SIGINT\", () => {\n void shutdown().finally(() => process.exit(0));\n});\nprocess.on(\"SIGTERM\", () => {\n void shutdown().finally(() => process.exit(0));\n});\n\nexport { meterProvider, sdk };\n","import dotenv from \"dotenv\";\ndotenv.config();\n\nimport { getConfig } from \"@powerhousedao/config/node\";\nimport type { DriveInput } from \"@powerhousedao/shared/document-drive\";\nimport { parseForcePgVersion } from \"./pglite-version.js\";\nconst phConfig = getConfig();\nconst { switchboard } = phConfig;\ninterface Config {\n database: {\n url: string;\n };\n port: number;\n mcp: boolean;\n migratePglite: boolean;\n forcePgVersion: 16 | 17 | null;\n drive: DriveInput;\n}\nexport const config: Config = {\n database: {\n // url: process.env.PH_SWITCHBOARD_DATABASE_URL ?? switchboard?.database?.url ?? \"dev.db\",\n url:\n process.env.PH_SWITCHBOARD_DATABASE_URL ??\n switchboard?.database?.url ??\n \"dev.db\",\n },\n port:\n process.env.PH_SWITCHBOARD_PORT &&\n !isNaN(Number(process.env.PH_SWITCHBOARD_PORT))\n ? Number(process.env.PH_SWITCHBOARD_PORT)\n : (switchboard?.port ?? 4001),\n mcp: true,\n migratePglite: process.env.PH_MIGRATE_PGLITE === \"true\",\n forcePgVersion: parseForcePgVersion(process.env.PH_FORCE_PG_VERSION),\n drive: {\n id: \"powerhouse\",\n slug: \"powerhouse\",\n global: {\n name: \"Powerhouse\",\n icon: \"https://ipfs.io/ipfs/QmcaTDBYn8X2psGaXe7iQ6qd8q6oqHLgxvMX9yXf7f9uP7\",\n },\n local: {\n availableOffline: true,\n listeners: [],\n sharingType: \"public\",\n triggers: [],\n },\n },\n};\n","import type { PyroscopeConfig } from \"@pyroscope/nodejs\";\n\nexport async function initProfilerFromEnv(env: typeof process.env) {\n const {\n PYROSCOPE_SERVER_ADDRESS: serverAddress,\n PYROSCOPE_APPLICATION_NAME: appName,\n PYROSCOPE_USER: basicAuthUser,\n PYROSCOPE_PASSWORD: basicAuthPassword,\n PYROSCOPE_WALL_ENABLED: wallEnabled,\n PYROSCOPE_HEAP_ENABLED: heapEnabled,\n } = env;\n\n const options: PyroscopeConfig = {\n serverAddress,\n appName,\n basicAuthUser,\n basicAuthPassword,\n // Wall profiling captures wall-clock time (includes async I/O waits)\n // This shows GraphQL resolvers even when waiting for database\n wall: {\n samplingDurationMs: 10000, // 10 second sampling windows\n samplingIntervalMicros: 10000, // 10ms sampling interval (100 samples/sec)\n collectCpuTime: true, // Also collect CPU time alongside wall time\n },\n // Heap profiling for memory allocation tracking\n heap: {\n samplingIntervalBytes: 512 * 1024, // Sample every 512KB allocated\n stackDepth: 64, // Capture deeper stacks for better context\n },\n };\n return initProfiler(options, {\n wallEnabled: wallEnabled !== \"false\",\n heapEnabled: heapEnabled === \"true\",\n });\n}\n\ninterface ProfilerFlags {\n wallEnabled?: boolean;\n heapEnabled?: boolean;\n}\n\nexport async function initProfiler(\n options?: PyroscopeConfig,\n flags: ProfilerFlags = { wallEnabled: true, heapEnabled: false },\n) {\n console.log(\"Initializing Pyroscope profiler at:\", options?.serverAddress);\n console.log(\" Wall profiling:\", flags.wallEnabled ? \"enabled\" : \"disabled\");\n console.log(\" Heap profiling:\", flags.heapEnabled ? \"enabled\" : \"disabled\");\n\n const { default: Pyroscope } = await import(\"@pyroscope/nodejs\");\n Pyroscope.init(options);\n\n // Start wall profiling (captures async I/O time - shows resolvers)\n if (flags.wallEnabled) {\n Pyroscope.startWallProfiling();\n }\n\n // Start CPU profiling (captures CPU-bound work)\n Pyroscope.startCpuProfiling();\n\n // Optionally start heap profiling (memory allocations)\n if (flags.heapEnabled) {\n Pyroscope.startHeapProfiling();\n }\n}\n","#!/usr/bin/env node\n// Observability MUST load before any module that imports http/express/pg/graphql\n// so OpenTelemetry's require-time hooks can patch them. It also owns Sentry\n// init and the SIGINT/SIGTERM flush.\nimport \"./observability.mjs\";\n\nimport * as Sentry from \"@sentry/node\";\nimport { childLogger } from \"document-model\";\nimport { config } from \"./config.js\";\nimport { initProfilerFromEnv } from \"./profiler.js\";\nimport { startSwitchboard } from \"./server.mjs\";\n\nconst logger = childLogger([\"switchboard\"]);\n\nfunction ensureNodeVersion(minVersion = \"24\") {\n const version = process.versions.node;\n if (!version) {\n return;\n }\n\n if (version < minVersion) {\n console.error(\n `Node version ${minVersion} or higher is required. Current version: ${version}`,\n );\n process.exit(1);\n }\n}\n// Ensure minimum Node.js version\nensureNodeVersion(\"24\");\n\n// Each subgraph registers its own SIGINT/SIGTERM listeners, and the count\n// scales with dynamically-loaded document models beyond the default cap of 10.\nprocess.setMaxListeners(0);\n\nif (process.env.PYROSCOPE_SERVER_ADDRESS) {\n try {\n await initProfilerFromEnv(process.env);\n } catch (e) {\n Sentry.captureException(e);\n logger.error(\"Error starting profiler: @error\", e);\n }\n}\n\nconst cliMigratePglite = process.argv.slice(2).includes(\"--migrate-pglite\");\n\nstartSwitchboard({\n ...config,\n migratePglite: cliMigratePglite || config.migratePglite,\n forcePgVersion: config.forcePgVersion ?? undefined,\n}).catch(console.error);\n"],"names":["logger","logger"],"mappings":"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","debug_id":"5ffe851c-e77f-513a-a99b-e5f1ba344644"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="50e38408-9102-5ba1-977a-69acc9cb0fe9")}catch(e){}}();
|
|
1
3
|
import path from "path";
|
|
2
4
|
import { execSync } from "child_process";
|
|
3
5
|
import fs from "fs";
|
|
@@ -28,4 +30,5 @@ try {
|
|
|
28
30
|
//#endregion
|
|
29
31
|
export {};
|
|
30
32
|
|
|
31
|
-
//# sourceMappingURL=install-packages.mjs.map
|
|
33
|
+
//# sourceMappingURL=install-packages.mjs.map
|
|
34
|
+
//# debugId=50e38408-9102-5ba1-977a-69acc9cb0fe9
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install-packages.mjs","
|
|
1
|
+
{"version":3,"file":"install-packages.mjs","sources":["../src/install-packages.mts"],"sourcesContent":["import { execSync } from \"child_process\";\nimport fs from \"fs\";\nimport path from \"path\";\n\n// Define interface for package.json\ninterface PackageJson {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\n// Get the list of packages to install from the environment variable\nconst pkgs = process.env.PH_PACKAGES?.split(\",\") || [];\n\n// Skip if no packages to install\nif (pkgs.length === 0 || (pkgs.length === 1 && pkgs[0] === \"\")) {\n process.exit(0);\n}\n\ntry {\n // Read the package.json file to check existing dependencies\n const packageJsonPath = path.join(process.cwd(), \"package.json\");\n const packageJsonContent = fs.readFileSync(packageJsonPath, \"utf-8\");\n const packageJson = JSON.parse(packageJsonContent) as PackageJson;\n\n // Get all installed dependencies\n const installedDependencies: Record<string, string> = {\n ...(packageJson.dependencies || {}),\n ...(packageJson.devDependencies || {}),\n };\n\n for (const pkg of pkgs) {\n if (pkg === \"\") continue;\n\n // Check if the package is already installed\n if (installedDependencies[pkg]) {\n console.log(`> Package ${pkg} is already installed, skipping`);\n continue;\n }\n\n console.log(`> Installing ${pkg}`);\n execSync(`pnpm add ${pkg}@latest`, { stdio: \"inherit\" });\n }\n} catch (error) {\n console.error(\"Error in package installation:\", error);\n process.exit(1);\n}\n"],"names":[],"mappings":";;;;;;AAWA,MAAM,OAAO,QAAQ,IAAI,aAAa,MAAM,IAAI,IAAI,EAAE;AAGtD,IAAI,KAAK,WAAW,KAAM,KAAK,WAAW,KAAK,KAAK,OAAO,GACzD,SAAQ,KAAK,EAAE;AAGjB,IAAI;CAEF,MAAM,kBAAkB,KAAK,KAAK,QAAQ,KAAK,EAAE,eAAe;CAChE,MAAM,qBAAqB,GAAG,aAAa,iBAAiB,QAAQ;CACpE,MAAM,cAAc,KAAK,MAAM,mBAAmB;CAGlD,MAAM,wBAAgD;EACpD,GAAI,YAAY,gBAAgB,EAAE;EAClC,GAAI,YAAY,mBAAmB,EAAE;EACtC;AAED,MAAK,MAAM,OAAO,MAAM;AACtB,MAAI,QAAQ,GAAI;AAGhB,MAAI,sBAAsB,MAAM;AAC9B,WAAQ,IAAI,aAAa,IAAI,iCAAiC;AAC9D;;AAGF,UAAQ,IAAI,gBAAgB,MAAM;AAClC,WAAS,YAAY,IAAI,UAAU,EAAE,OAAO,WAAW,CAAC;;SAEnD,OAAO;AACd,SAAQ,MAAM,kCAAkC,MAAM;AACtD,SAAQ,KAAK,EAAE","debug_id":"50e38408-9102-5ba1-977a-69acc9cb0fe9"}
|
package/dist/migrate.mjs
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="089fa7e5-3ec6-5326-9efc-05a1101d2bd2")}catch(e){}}();
|
|
4
|
+
import dotenv from "dotenv";
|
|
2
5
|
import { getConfig } from "@powerhousedao/config/node";
|
|
3
6
|
import { REACTOR_SCHEMA, getMigrationStatus, runMigrations } from "@powerhousedao/reactor";
|
|
4
7
|
import { Kysely, PostgresDialect } from "kysely";
|
|
5
8
|
import { Pool } from "pg";
|
|
6
9
|
//#region src/migrate.mts
|
|
10
|
+
dotenv.config();
|
|
7
11
|
function isPostgresUrl(url) {
|
|
8
12
|
return url.startsWith("postgresql://") || url.startsWith("postgres://");
|
|
9
13
|
}
|
|
10
14
|
async function main() {
|
|
11
15
|
const command = process.argv[2];
|
|
12
16
|
const config = getConfig();
|
|
13
|
-
const dbPath = process.env.PH_REACTOR_DATABASE_URL ?? process.env.DATABASE_URL ?? config.switchboard?.database?.url;
|
|
17
|
+
const dbPath = process.env.PH_SWITCHBOARD_DATABASE_URL ?? process.env.PH_REACTOR_DATABASE_URL ?? process.env.DATABASE_URL ?? config.switchboard?.database?.url;
|
|
14
18
|
if (!dbPath || !isPostgresUrl(dbPath)) {
|
|
15
19
|
console.log("No PostgreSQL URL configured. Skipping migrations.");
|
|
16
20
|
console.log("(PGlite migrations are handled automatically on startup)");
|
|
@@ -52,4 +56,5 @@ main();
|
|
|
52
56
|
//#endregion
|
|
53
57
|
export {};
|
|
54
58
|
|
|
55
|
-
//# sourceMappingURL=migrate.mjs.map
|
|
59
|
+
//# sourceMappingURL=migrate.mjs.map
|
|
60
|
+
//# debugId=089fa7e5-3ec6-5326-9efc-05a1101d2bd2
|
package/dist/migrate.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate.mjs","
|
|
1
|
+
{"version":3,"file":"migrate.mjs","sources":["../src/migrate.mts"],"sourcesContent":["#!/usr/bin/env node\nimport dotenv from \"dotenv\";\ndotenv.config();\n\nimport { Kysely, PostgresDialect } from \"kysely\";\nimport { Pool } from \"pg\";\nimport {\n runMigrations,\n getMigrationStatus,\n REACTOR_SCHEMA,\n} from \"@powerhousedao/reactor\";\nimport { getConfig } from \"@powerhousedao/config/node\";\n\nfunction isPostgresUrl(url: string): boolean {\n return url.startsWith(\"postgresql://\") || url.startsWith(\"postgres://\");\n}\n\nasync function main() {\n const command = process.argv[2];\n const config = getConfig();\n\n const dbPath =\n process.env.PH_SWITCHBOARD_DATABASE_URL ??\n process.env.PH_REACTOR_DATABASE_URL ??\n process.env.DATABASE_URL ??\n config.switchboard?.database?.url;\n\n if (!dbPath || !isPostgresUrl(dbPath)) {\n console.log(\"No PostgreSQL URL configured. Skipping migrations.\");\n console.log(\"(PGlite migrations are handled automatically on startup)\");\n return;\n }\n\n console.log(`Database: ${dbPath}`);\n\n const pool = new Pool({ connectionString: dbPath });\n\n const db = new Kysely<any>({\n dialect: new PostgresDialect({ pool }),\n });\n\n try {\n if (command === \"status\") {\n console.log(\"\\nChecking migration status...\");\n const migrations = await getMigrationStatus(db, REACTOR_SCHEMA);\n\n console.log(\"\\nMigration Status:\");\n console.log(\"=================\");\n\n for (const migration of migrations) {\n const status = migration.executedAt\n ? `[OK] Executed at ${migration.executedAt.toISOString()}`\n : \"[--] Pending\";\n console.log(`${status} - ${migration.name}`);\n }\n } else {\n console.log(\"\\nRunning migrations...\");\n const result = await runMigrations(db, REACTOR_SCHEMA);\n\n if (!result.success) {\n console.error(\"Migration failed:\", result.error?.message);\n process.exit(1);\n }\n\n if (result.migrationsExecuted.length === 0) {\n console.log(\"No migrations to run - database is up to date\");\n } else {\n console.log(\n `Successfully executed ${result.migrationsExecuted.length} migration(s):`,\n );\n for (const name of result.migrationsExecuted) {\n console.log(` - ${name}`);\n }\n }\n }\n } catch (error) {\n console.error(\n \"Error:\",\n error instanceof Error ? error.message : String(error),\n );\n process.exit(1);\n } finally {\n await db.destroy();\n }\n}\n\nvoid main();\n"],"names":[],"mappings":";;;;;;;;;AAEA,OAAO,QAAQ;AAWf,SAAS,cAAc,KAAsB;AAC3C,QAAO,IAAI,WAAW,gBAAgB,IAAI,IAAI,WAAW,cAAc;;AAGzE,eAAe,OAAO;CACpB,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,SAAS,WAAW;CAE1B,MAAM,SACJ,QAAQ,IAAI,+BACZ,QAAQ,IAAI,2BACZ,QAAQ,IAAI,gBACZ,OAAO,aAAa,UAAU;AAEhC,KAAI,CAAC,UAAU,CAAC,cAAc,OAAO,EAAE;AACrC,UAAQ,IAAI,qDAAqD;AACjE,UAAQ,IAAI,2DAA2D;AACvE;;AAGF,SAAQ,IAAI,aAAa,SAAS;CAIlC,MAAM,KAAK,IAAI,OAAY,EACzB,SAAS,IAAI,gBAAgB,EAAE,MAHpB,IAAI,KAAK,EAAE,kBAAkB,QAAQ,CAAC,EAGZ,CAAC,EACvC,CAAC;AAEF,KAAI;AACF,MAAI,YAAY,UAAU;AACxB,WAAQ,IAAI,iCAAiC;GAC7C,MAAM,aAAa,MAAM,mBAAmB,IAAI,eAAe;AAE/D,WAAQ,IAAI,sBAAsB;AAClC,WAAQ,IAAI,oBAAoB;AAEhC,QAAK,MAAM,aAAa,YAAY;IAClC,MAAM,SAAS,UAAU,aACrB,oBAAoB,UAAU,WAAW,aAAa,KACtD;AACJ,YAAQ,IAAI,GAAG,OAAO,KAAK,UAAU,OAAO;;SAEzC;AACL,WAAQ,IAAI,0BAA0B;GACtC,MAAM,SAAS,MAAM,cAAc,IAAI,eAAe;AAEtD,OAAI,CAAC,OAAO,SAAS;AACnB,YAAQ,MAAM,qBAAqB,OAAO,OAAO,QAAQ;AACzD,YAAQ,KAAK,EAAE;;AAGjB,OAAI,OAAO,mBAAmB,WAAW,EACvC,SAAQ,IAAI,gDAAgD;QACvD;AACL,YAAQ,IACN,yBAAyB,OAAO,mBAAmB,OAAO,gBAC3D;AACD,SAAK,MAAM,QAAQ,OAAO,mBACxB,SAAQ,IAAI,OAAO,OAAO;;;UAIzB,OAAO;AACd,UAAQ,MACN,UACA,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,CACvD;AACD,UAAQ,KAAK,EAAE;WACP;AACR,QAAM,GAAG,SAAS;;;AAIjB,MAAM","debug_id":"089fa7e5-3ec6-5326-9efc-05a1101d2bd2"}
|