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 +56 -2
- package/package.json +41 -5
- package/src/appLogger.js +44 -0
- package/src/index.d.ts +36 -0
- package/src/index.js +125 -0
package/README.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
-
#
|
|
1
|
+
# graftcode-appinsights-connector (Node.js)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|
package/src/appLogger.js
ADDED
|
@@ -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
|
+
}
|