graftcode-appinsights-connector 0.0.0-stage → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,57 @@
1
- # Temporary Holding Version
1
+ # graftcode-appinsights-connector (Node.js)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ One-call Azure Application Insights setup for Node.js services that use Graftcode: traces, logs, and Graftcode call instrumentation.
4
+
5
+ ## Quick start
6
+
7
+ Call once, as early as possible. For a web server, load before the HTTP framework (e.g. `node --import ./telemetry.js server.js`):
8
+
9
+ ```js
10
+ import { useGraftCodeAppInsights } from 'graftcode-appinsights-connector'
11
+
12
+ export const telemetry = useGraftCodeAppInsights({
13
+ serviceName: 'MyService',
14
+ graftcode: {
15
+ libraries: { 'ws://localhost:9180/ws': 'MyNodeLib' },
16
+ },
17
+ })
18
+ ```
19
+
20
+ ## Options (`GraftCodeAppInsightsOptions`)
21
+
22
+ | Option | Description |
23
+ |--------|-------------|
24
+ | `serviceName` | Cloud role name in Application Insights (required). |
25
+ | `connectionString` | Defaults to `APPLICATIONINSIGHTS_CONNECTION_STRING`. |
26
+ | `tracesPerSecond` | Rate-limit sampling; default keeps every trace (`0`). |
27
+ | `samplingRatio` | Fixed ratio 0–1 when `tracesPerSecond` is not used; default `1`. |
28
+ | `logLevel` | Lowest level for app and Graftcode logs: `debug`, `info`, `warning`, `error`. Default `info`. |
29
+ | `console` | Also print app logs to stdout. Default `true`. |
30
+ | `graftcode` | `GraftcodeInstrumentation` options, or `false` to omit Graftcode instrumentation. |
31
+ | `instrumentations` | Additional OpenTelemetry instrumentations to register. |
32
+ | `azureMonitor` | Advanced `@azure/monitor-opentelemetry` options (merged first). |
33
+
34
+ ## Return value (`GraftCodeTelemetry`)
35
+
36
+ | Member | Description |
37
+ |--------|-------------|
38
+ | `serviceName` | Configured role name. |
39
+ | `isExporting` | `true` when a connection string is configured. |
40
+ | `tracer` | OpenTelemetry tracer for the service. |
41
+ | `logger` | App logger (`debug`, `info`, `warn`, `error`) exported to OpenTelemetry logs. |
42
+ | `flush()` | Sends buffered telemetry now. |
43
+ | `shutdown()` | Flush and stop; safe to call more than once. |
44
+
45
+ `useGraftCodeAppInsights` is single-process: a second call with the same `serviceName` returns the existing instance; a different name throws.
46
+
47
+ ## Without a connection string
48
+
49
+ Telemetry is not exported to Azure, but a local `NodeTracerProvider` still assigns real trace IDs so context flows through Graftcode calls. A warning is logged once.
50
+
51
+ ## HTTP auto-instrumentation
52
+
53
+ Azure Monitor HTTP instrumentation stays enabled. Outgoing requests that are WebSocket upgrades (`Upgrade: websocket`) are ignored so the long-lived handshake is not tied to a single HTTP request; each Graft call is already described by the Graftcode span.
54
+
55
+ ## App logger
56
+
57
+ Application logs use OpenTelemetry Logs with severity mapping and optional console output (`label: serviceName: message`). Errors attach redacted exception attributes.
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
- "name": "graftcode-appinsights-connector",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "graftcode-appinsights-connector",
3
+ "version": "1.0.3",
4
+ "description": "One-call Azure Application Insights setup for Node.js services that use Graftcode: traces, logs and Graftcode call instrumentation.",
5
+ "license": "ISC",
6
+ "author": "Graftcode S.A.",
7
+ "type": "module",
8
+ "main": "./src/index.js",
9
+ "types": "./src/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.d.ts",
13
+ "default": "./src/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "README.md",
18
+ "src"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "scripts": {
24
+ "test": "node --test \"test/**/*.test.js\""
25
+ },
26
+ "dependencies": {
27
+ "@azure/monitor-opentelemetry": "^1.20.1",
28
+ "@opentelemetry/api": "^1.9.1",
29
+ "@opentelemetry/api-logs": "^0.221.0",
30
+ "@opentelemetry/instrumentation": "^0.221.0",
31
+ "@opentelemetry/resources": "^2.10.0",
32
+ "@opentelemetry/sdk-trace-node": "^2.10.0"
33
+ },
34
+ "peerDependencies": {
35
+ "graftcode-instrumentation": "^1.0.0",
36
+ "hypertube-nodejs-sdk": "*"
37
+ },
38
+ "devDependencies": {
39
+ "graftcode-instrumentation": "file:../../../Instrumentation/nodejs",
40
+ "hypertube-nodejs-sdk": "file:../../../../../NewHT/HYPERTUBE/src/js/hypertube-nodejs-sdk"
41
+ }
42
+ }
@@ -0,0 +1,44 @@
1
+ import { context } from '@opentelemetry/api'
2
+ import { logs } from '@opentelemetry/api-logs'
3
+ import { LogsLevel, LogsRedactor } from 'hypertube-nodejs-sdk/abstractions'
4
+
5
+ const SEVERITIES = {
6
+ [LogsLevel.Debug]: [5, 'DEBUG', 'dbug'],
7
+ [LogsLevel.Info]: [9, 'INFO', 'info'],
8
+ [LogsLevel.Warning]: [13, 'WARN', 'warn'],
9
+ [LogsLevel.Error]: [17, 'ERROR', 'fail'],
10
+ }
11
+
12
+ export function createAppLogger(serviceName, minimumLevel, toConsole) {
13
+ const logger = logs.getLogger(serviceName)
14
+
15
+ const write = (level, message, details) => {
16
+ if (level < minimumLevel) {
17
+ return
18
+ }
19
+
20
+ const [severityNumber, severityText, label] = SEVERITIES[level]
21
+ const attributes = details instanceof Error ? exceptionAttributes(details) : { ...details }
22
+ logger.emit({ severityNumber, severityText, body: message, attributes, context: context.active() })
23
+
24
+ if (toConsole) {
25
+ const suffix = details instanceof Error ? ` ${details.name}: ${details.message}` : ''
26
+ process.stdout.write(`${label}: ${serviceName}: ${message}${suffix}\n`)
27
+ }
28
+ }
29
+
30
+ return {
31
+ debug: (message, attributes) => write(LogsLevel.Debug, message, attributes),
32
+ info: (message, attributes) => write(LogsLevel.Info, message, attributes),
33
+ warn: (message, attributes) => write(LogsLevel.Warning, message, attributes),
34
+ error: (message, errorOrAttributes) => write(LogsLevel.Error, message, errorOrAttributes),
35
+ }
36
+ }
37
+
38
+ function exceptionAttributes(error) {
39
+ return {
40
+ 'exception.type': error.name,
41
+ 'exception.message': String(LogsRedactor.redactValue(error.message) ?? ''),
42
+ 'exception.stacktrace': error.stack ? LogsRedactor.redactBody(error.stack) : undefined,
43
+ }
44
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { AzureMonitorOpenTelemetryOptions } from '@azure/monitor-opentelemetry'
2
+ import type { Tracer } from '@opentelemetry/api'
3
+ import type { Instrumentation } from '@opentelemetry/instrumentation'
4
+ import type { GraftcodeInstrumentationConfig } from 'graftcode-instrumentation'
5
+
6
+ export type LogAttributes = Record<string, string | number | boolean | undefined>
7
+
8
+ export interface GraftCodeAppInsightsOptions {
9
+ serviceName: string
10
+ connectionString?: string
11
+ tracesPerSecond?: number
12
+ samplingRatio?: number
13
+ logLevel?: 'debug' | 'info' | 'warning' | 'error'
14
+ console?: boolean
15
+ graftcode?: GraftcodeInstrumentationConfig | false
16
+ instrumentations?: Instrumentation[]
17
+ azureMonitor?: AzureMonitorOpenTelemetryOptions
18
+ }
19
+
20
+ export interface GraftCodeAppLogger {
21
+ debug(message: string, attributes?: LogAttributes): void
22
+ info(message: string, attributes?: LogAttributes): void
23
+ warn(message: string, attributes?: LogAttributes): void
24
+ error(message: string, errorOrAttributes?: Error | LogAttributes): void
25
+ }
26
+
27
+ export interface GraftCodeTelemetry {
28
+ readonly serviceName: string
29
+ readonly isExporting: boolean
30
+ readonly tracer: Tracer
31
+ readonly logger: GraftCodeAppLogger
32
+ flush(): Promise<void>
33
+ shutdown(): Promise<void>
34
+ }
35
+
36
+ export declare function useGraftCodeAppInsights(options: GraftCodeAppInsightsOptions): GraftCodeTelemetry
package/src/index.js ADDED
@@ -0,0 +1,125 @@
1
+ import { shutdownAzureMonitor, useAzureMonitor } from '@azure/monitor-opentelemetry'
2
+ import { trace } from '@opentelemetry/api'
3
+ import { logs } from '@opentelemetry/api-logs'
4
+ import { registerInstrumentations } from '@opentelemetry/instrumentation'
5
+ import { resourceFromAttributes } from '@opentelemetry/resources'
6
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
7
+ import { GraftcodeInstrumentation } from 'graftcode-instrumentation'
8
+ import { LogsLevel, parseLogsLevel } from 'hypertube-nodejs-sdk/abstractions'
9
+ import { createAppLogger } from './appLogger.js'
10
+
11
+ let active
12
+
13
+ export function useGraftCodeAppInsights(options) {
14
+ const serviceName = options?.serviceName?.trim()
15
+ if (!serviceName) {
16
+ throw new TypeError('useGraftCodeAppInsights needs a serviceName')
17
+ }
18
+ if (active) {
19
+ if (active.serviceName === serviceName) {
20
+ return active
21
+ }
22
+ throw new Error(`GraftCode Application Insights telemetry is already started for ${active.serviceName}`)
23
+ }
24
+
25
+ const connectionString =
26
+ options.connectionString?.trim() || process.env.APPLICATIONINSIGHTS_CONNECTION_STRING?.trim() || undefined
27
+ const logLevel = parseLogsLevel(options.logLevel ?? 'info') ?? LogsLevel.Info
28
+ const stopProviders = connectionString
29
+ ? startAzureMonitor(serviceName, connectionString, options)
30
+ : startLocalTracing(serviceName)
31
+
32
+ const graftcode =
33
+ options.graftcode === false
34
+ ? []
35
+ : [new GraftcodeInstrumentation({ logLevel: options.logLevel ?? 'info', ...options.graftcode })]
36
+ const unregisterInstrumentations = registerInstrumentations({
37
+ instrumentations: [...graftcode, ...(options.instrumentations ?? [])],
38
+ })
39
+
40
+ let stopping
41
+ const telemetry = {
42
+ serviceName,
43
+ isExporting: connectionString !== undefined,
44
+ tracer: trace.getTracer(serviceName),
45
+ logger: createAppLogger(serviceName, logLevel, options.console !== false),
46
+ flush: forceFlush,
47
+ shutdown: () => {
48
+ stopping ??= (async () => {
49
+ process.off('beforeExit', onBeforeExit)
50
+ await forceFlush()
51
+ unregisterInstrumentations()
52
+ await stopProviders()
53
+ active = undefined
54
+ })()
55
+ return stopping
56
+ },
57
+ }
58
+ const onBeforeExit = () => {
59
+ void telemetry.shutdown()
60
+ }
61
+ process.once('beforeExit', onBeforeExit)
62
+
63
+ active = telemetry
64
+ if (!connectionString) {
65
+ telemetry.logger.warn(
66
+ `Application Insights telemetry for ${serviceName} is disabled: no connection string was configured. ` +
67
+ 'Set APPLICATIONINSIGHTS_CONNECTION_STRING in this process or pass connectionString.'
68
+ )
69
+ }
70
+ return telemetry
71
+ }
72
+
73
+ function startAzureMonitor(serviceName, connectionString, options) {
74
+ const advanced = options.azureMonitor ?? {}
75
+ const http = advanced.instrumentationOptions?.http
76
+ const userIgnoreOutgoing = http?.ignoreOutgoingRequestHook
77
+
78
+ useAzureMonitor({
79
+ ...advanced,
80
+ azureMonitorExporterOptions: { ...advanced.azureMonitorExporterOptions, connectionString },
81
+ resource: resourceFromAttributes({ 'service.name': serviceName }),
82
+ tracesPerSecond: options.tracesPerSecond ?? 0,
83
+ samplingRatio: options.samplingRatio ?? 1,
84
+ instrumentationOptions: {
85
+ ...advanced.instrumentationOptions,
86
+ http: {
87
+ enabled: true,
88
+ ...http,
89
+ ignoreOutgoingRequestHook: (request) =>
90
+ isWebSocketUpgrade(request) || Boolean(userIgnoreOutgoing?.(request)),
91
+ },
92
+ },
93
+ })
94
+ return () => shutdownAzureMonitor()
95
+ }
96
+
97
+ function startLocalTracing(serviceName) {
98
+ const provider = new NodeTracerProvider({ resource: resourceFromAttributes({ 'service.name': serviceName }) })
99
+ provider.register()
100
+ return () => provider.shutdown()
101
+ }
102
+
103
+ function isWebSocketUpgrade(request) {
104
+ const headers = request?.headers ?? {}
105
+ return Object.entries(headers).some(
106
+ ([name, value]) => name.toLowerCase() === 'upgrade' && String(value).toLowerCase() === 'websocket'
107
+ )
108
+ }
109
+
110
+ async function forceFlush() {
111
+ await Promise.allSettled([
112
+ delegateOf(trace.getTracerProvider())?.forceFlush?.(),
113
+ delegateOf(logs.getLoggerProvider())?.forceFlush?.(),
114
+ ])
115
+ }
116
+
117
+ function delegateOf(provider) {
118
+ if (typeof provider?.getDelegate === 'function') {
119
+ return provider.getDelegate()
120
+ }
121
+ if (typeof provider?._getDelegate === 'function') {
122
+ return provider._getDelegate()
123
+ }
124
+ return provider
125
+ }