@open-mercato/telemetry 0.10.1-develop.7894.1.2c464c73e3 → 0.10.1-develop.7903.1.d8ab1c8b0d

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/telemetry",
3
- "version": "0.10.1-develop.7894.1.2c464c73e3",
3
+ "version": "0.10.1-develop.7903.1.d8ab1c8b0d",
4
4
  "license": "MIT",
5
5
  "description": "Vendor-neutral spans, metrics, and error reporting backed by a pluggable OTLP provider. Off by default; the telemetry runtime is loaded only when explicitly enabled.",
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  }
39
39
  },
40
40
  "dependencies": {
41
- "@open-mercato/shared": "0.10.1-develop.7894.1.2c464c73e3"
41
+ "@open-mercato/shared": "0.10.1-develop.7903.1.d8ab1c8b0d"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": ">=18.0.0"
@@ -16,10 +16,13 @@ import path from 'node:path'
16
16
  * a dead port still drives the query path, and the instrumentation records the
17
17
  * span before the underlying call fails.
18
18
  */
19
- const distProvider = path.resolve(__dirname, '../../dist/provider/otlp-provider.js')
20
19
  const pkgRoot = path.resolve(__dirname, '../..')
21
20
 
22
21
  const CHILD_SCRIPT = `
22
+ import assert from 'node:assert/strict'
23
+ import { writeSync } from 'node:fs'
24
+ const stage = (name) => writeSync(2, 'STAGE=' + name + '\\n')
25
+ stage('import-provider')
23
26
  const { OtlpProvider } = await import('@open-mercato/telemetry/provider/otlp-provider')
24
27
  const { setActiveProvider } = await import('@open-mercato/telemetry/provider/registry')
25
28
  const { InMemorySpanExporter, SimpleSpanProcessor } = await import('@opentelemetry/sdk-trace-base')
@@ -27,34 +30,59 @@ const { InMemorySpanExporter, SimpleSpanProcessor } = await import('@opentelemet
27
30
  // No 'instrumentations' override: the provider's DEFAULT list must include the
28
31
  // pg instrumentation (with PII-safe options), or this probe records no span.
29
32
  const exporter = new InMemorySpanExporter()
33
+ const processor = new SimpleSpanProcessor(exporter)
30
34
  const provider = new OtlpProvider({
31
- spanProcessors: [new SimpleSpanProcessor(exporter)],
35
+ spanProcessors: [processor],
36
+ logRecordProcessors: [],
37
+ metricReaders: [],
32
38
  })
33
39
  // start() BEFORE pg is imported — the ordering the CLI bootstrap guarantees.
40
+ stage('start-provider')
34
41
  await provider.start()
35
42
  setActiveProvider(provider)
36
43
 
44
+ stage('import-pg')
37
45
  const pgModule = await import('pg')
38
46
  const Client = (pgModule.default ?? pgModule).Client
39
- const client = new Client({ host: '127.0.0.1', port: 1, connectionTimeoutMillis: 200 })
40
- try { await client.connect() } catch {}
41
- try { await client.query('SELECT $1::text', ['pii-sentinel-value']) } catch {}
42
- await new Promise((resolve) => setTimeout(resolve, 250))
43
- const spans = exporter.getFinishedSpans().map((s) => ({ name: s.name, attributes: s.attributes }))
44
- console.log('SPANS=' + JSON.stringify(spans))
45
- process.exit(0)
47
+ const client = new Client({
48
+ host: '127.0.0.1', port: 1, connectionTimeoutMillis: 200,
49
+ user: 'telemetry-probe', database: 'telemetry-probe',
50
+ })
51
+ const ended = new Promise((resolve) => client.once('end', resolve))
52
+ try {
53
+ stage('connect')
54
+ await assert.rejects(client.connect())
55
+ if (process.argv[2] === 'socket-ended') {
56
+ stage('wait-socket-end')
57
+ await ended
58
+ }
59
+ stage('close-client')
60
+ await client.end()
61
+ stage('query')
62
+ await assert.rejects(client.query('SELECT $1::text', ['pii-sentinel-value']))
63
+ stage('flush-spans')
64
+ await processor.forceFlush()
65
+ const spans = exporter.getFinishedSpans().map((span) => ({ name: span.name, attributes: span.attributes }))
66
+ stage('shutdown-provider')
67
+ await provider.shutdown()
68
+ stage('shutdown-finished')
69
+ console.log('SPANS=' + JSON.stringify(spans))
70
+ } finally {
71
+ await client.end()
72
+ await provider.shutdown()
73
+ }
46
74
  `
47
75
 
48
76
  type ProbeSpan = { name: string; attributes: Record<string, unknown> }
49
77
 
50
- function runChild(): ProbeSpan[] {
78
+ function runChild(order: string): ProbeSpan[] {
51
79
  // The script must live inside the package tree: ESM resolves bare specifiers
52
80
  // (`@open-mercato/telemetry`, `@opentelemetry/*`, `pg`) relative to the file's
53
81
  // location, not the process cwd.
54
82
  const scriptFile = path.join(pkgRoot, `.pg-instrumentation-probe.${process.pid}.mjs`)
55
83
  fs.writeFileSync(scriptFile, CHILD_SCRIPT)
56
84
  try {
57
- const result = spawnSync(process.execPath, [scriptFile], {
85
+ const result = spawnSync(process.execPath, [scriptFile, order], {
58
86
  cwd: pkgRoot,
59
87
  encoding: 'utf8',
60
88
  env: {
@@ -65,24 +93,26 @@ function runChild(): ProbeSpan[] {
65
93
  },
66
94
  timeout: 30_000,
67
95
  })
68
- const line = (result.stdout || '').split('\n').find((l) => l.startsWith('SPANS='))
69
- if (!line) throw new Error(`child produced no SPANS output.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`)
96
+ const line = (result.stdout || '').split('\n').find((candidate) => candidate.startsWith('SPANS='))
97
+ if (result.error || result.status !== 0 || result.signal || !line) {
98
+ throw new Error(`[internal] child probe failed.\nerror: ${result.error?.message ?? 'none'}\nstatus: ${result.status}\nsignal: ${result.signal}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`)
99
+ }
70
100
  return JSON.parse(line.slice('SPANS='.length)) as ProbeSpan[]
71
101
  } finally {
72
102
  fs.rmSync(scriptFile, { force: true })
73
103
  }
74
104
  }
75
105
 
76
- // The build output must exist (the child imports the compiled provider). Runs in
77
- // CI after build:packages; skip locally when the package hasn't been built yet.
78
- const maybe = fs.existsSync(distProvider) ? describe : describe.skip
79
-
80
- maybe('OTLP provider pg instrumentation', () => {
81
- it('emits a pg query span (default instrumentations) without leaking bound parameter values', () => {
82
- const spans = runChild()
83
- expect(spans.some((span) => span.name.startsWith('pg.query'))).toBe(true)
84
- // enhancedDatabaseReporting must stay off in the provider's default wiring:
85
- // the statement text may appear, bound values must not.
86
- expect(JSON.stringify(spans)).not.toContain('pii-sentinel-value')
87
- }, 35_000)
106
+ describe('OTLP provider pg instrumentation', () => {
107
+ it.each(['connect-rejected', 'socket-ended'])(
108
+ 'emits a pg query span without leaking bound parameter values after %s',
109
+ (order) => {
110
+ const spans = runChild(order)
111
+ expect(spans.some((span) => span.name.startsWith('pg.query'))).toBe(true)
112
+ // enhancedDatabaseReporting must stay off in the provider's default wiring:
113
+ // the statement text may appear, bound values must not.
114
+ expect(JSON.stringify(spans)).not.toContain('pii-sentinel-value')
115
+ },
116
+ 35_000,
117
+ )
88
118
  })