graftcode-instrumentation 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 +55 -2
- package/package.json +39 -5
- package/src/GraftcodeInstrumentation.js +116 -0
- package/src/bodyCapture.js +109 -0
- package/src/callSpan.js +257 -0
- package/src/graftcodeTelemetrySettings.js +77 -0
- package/src/index.d.ts +39 -0
- package/src/index.js +2 -0
- package/src/libraryCatalog.js +189 -0
- package/src/operationName.js +139 -0
- package/src/resolveOperation.js +104 -0
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# graftcode-instrumentation (Node.js)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenTelemetry instrumentation for Graftcode (Hypertube) calls: client and server spans, W3C trace context propagation, and Hypertube log forwarding.
|
|
4
|
+
|
|
5
|
+
Hypertube exposes a **broker** (`HypertubeBroker.attach` / `detach`); nothing is monkey-patched. Enabling attaches a broker listener and a log listener; disabling removes them.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Register like any other OpenTelemetry instrumentation:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { registerInstrumentations } from '@opentelemetry/instrumentation'
|
|
13
|
+
import { GraftcodeInstrumentation } from 'graftcode-instrumentation'
|
|
14
|
+
|
|
15
|
+
registerInstrumentations({ instrumentations: [new GraftcodeInstrumentation()] })
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Configuration (`GraftcodeInstrumentationConfig`)
|
|
19
|
+
|
|
20
|
+
| Option | Description |
|
|
21
|
+
|--------|-------------|
|
|
22
|
+
| `enabled` | Default `true`. |
|
|
23
|
+
| `captureBodies` | Record redacted request and response payloads on spans. Defaults to env `HYPERTUBE_TELEMETRY_CAPTURE_BODIES`, otherwise `false`. |
|
|
24
|
+
| `maxBodyLength` | Characters kept per payload, 256–32768. Defaults to `HYPERTUBE_TELEMETRY_BODY_MAX_CHARS`, otherwise `8192`. |
|
|
25
|
+
| `propagateContext` | Send W3C trace context with outgoing calls. Defaults to `HYPERTUBE_TELEMETRY_AUTO_INJECT`, otherwise `true`. |
|
|
26
|
+
| `logLevel` | Lowest Hypertube log level forwarded to OpenTelemetry logs (`trace`, `debug`, `info`, `warning`, `error`, `critical`, `off`). Default `info`. |
|
|
27
|
+
| `ignore` | `(operation) => boolean` — return `true` to skip tracing an operation. |
|
|
28
|
+
| `libraries` | Names the library behind a connection for the `Graft <library> <channel>` dependency name. Map from connection address (e.g. `ws://localhost:9180/ws`) to library name, or a function `(target) => string \| undefined`. Node.js types carry no namespace; without this, the caller falls back to `Graft <runtime> <channel>` unless the call loads the library. |
|
|
29
|
+
|
|
30
|
+
## Span model
|
|
31
|
+
|
|
32
|
+
- **Caller:** span name `Graft <library> [version] <channel>` (or runtime fallback), kind `INTERNAL`, attributes `graft.dependency.type` = `Graft`, `graft.dependency.target`, plus Hypertube operation metadata.
|
|
33
|
+
- **Callee:** span name `Service.method` (or `method` for globals), kind `INTERNAL`.
|
|
34
|
+
- Remote failures that return an `Exception` command mark the span failed from that response (not only thrown errors).
|
|
35
|
+
|
|
36
|
+
## Trace context
|
|
37
|
+
|
|
38
|
+
- Outgoing: W3C `traceparent` / `tracestate` via `callContext.outgoingHeaders`, plus `traceid` when sampled.
|
|
39
|
+
- Incoming: prefers W3C headers; a Graftcode `traceid` header alone still joins that trace under a synthetic remote parent.
|
|
40
|
+
- A `traceparent` already on the connection is not overridden.
|
|
41
|
+
|
|
42
|
+
## Library catalog
|
|
43
|
+
|
|
44
|
+
`LibraryCatalog.record` remembers libraries loaded on a connection (`LoadLibrary`) so later calls can be attributed. `LibraryCatalog.describeTarget` formats `<library> [version] <channel>`. Version is read from local `package.json` when available. Package folder naming rules apply for `package.json`, `index.js`, `main.js`, etc.
|
|
45
|
+
|
|
46
|
+
## Body capture
|
|
47
|
+
|
|
48
|
+
When enabled, request/response bodies are JSON-like renderings with redaction and truncation (`hypertube.request.body`, `hypertube.response.body`, `.truncated` when clipped).
|
|
49
|
+
|
|
50
|
+
## Operations (`resolveOperation`)
|
|
51
|
+
|
|
52
|
+
Maps Hypertube commands to `GraftOperation`: `name` (`Service.method` or `method`), `service`, `method`, `commandType`. Skips non-member / control commands without a resolvable method name.
|
|
53
|
+
|
|
54
|
+
## Log bridge
|
|
55
|
+
|
|
56
|
+
Forwards Hypertube log records to the OpenTelemetry Logs API, correlated with the active span, respecting configured minimum level and redaction on errors.
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "graftcode-instrumentation",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "OpenTelemetry instrumentation for Graftcode (Hypertube) calls in Node.js: client and server spans, W3C trace context propagation and Hypertube log forwarding.",
|
|
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
|
+
"@opentelemetry/instrumentation": "^0.221.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@opentelemetry/api": "^1.9.0",
|
|
31
|
+
"hypertube-nodejs-sdk": "*"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@opentelemetry/api": "^1.9.1",
|
|
35
|
+
"@opentelemetry/sdk-logs": "^0.221.0",
|
|
36
|
+
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
37
|
+
"@opentelemetry/sdk-trace-node": "^2.10.0",
|
|
38
|
+
"hypertube-nodejs-sdk": "file:../../../../NewHT/HYPERTUBE/src/js/hypertube-nodejs-sdk"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { context } from '@opentelemetry/api'
|
|
3
|
+
import { InstrumentationBase } from '@opentelemetry/instrumentation'
|
|
4
|
+
import {
|
|
5
|
+
HypertubeBroker,
|
|
6
|
+
HypertubeLogger,
|
|
7
|
+
LogsCommandResolver,
|
|
8
|
+
LogsLevel,
|
|
9
|
+
LogsRedactor,
|
|
10
|
+
} from 'hypertube-nodejs-sdk/abstractions'
|
|
11
|
+
import { CallSpan, completeSpans } from './callSpan.js'
|
|
12
|
+
import { resolveInstrumentationOptions } from './graftcodeTelemetrySettings.js'
|
|
13
|
+
|
|
14
|
+
const { version: PACKAGE_VERSION } = createRequire(import.meta.url)('../package.json')
|
|
15
|
+
|
|
16
|
+
export const INSTRUMENTATION_NAME = 'Graftcode'
|
|
17
|
+
export const LOGGER_NAME = 'Hypertube'
|
|
18
|
+
|
|
19
|
+
const SEVERITIES = [
|
|
20
|
+
[1, 'TRACE'],
|
|
21
|
+
[5, 'DEBUG'],
|
|
22
|
+
[9, 'INFO'],
|
|
23
|
+
[13, 'WARN'],
|
|
24
|
+
[17, 'ERROR'],
|
|
25
|
+
[21, 'FATAL'],
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
export class GraftcodeInstrumentation extends InstrumentationBase {
|
|
29
|
+
constructor(config = {}) {
|
|
30
|
+
super(INSTRUMENTATION_NAME, PACKAGE_VERSION, config)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
init() {
|
|
34
|
+
return []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
enable() {
|
|
38
|
+
if (this._detachFromHypertube) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const getSettings = () => this._currentSettings()
|
|
43
|
+
const listener = {
|
|
44
|
+
onExecute: (callContext, command, connectionData) => {
|
|
45
|
+
const reported = LogsCommandResolver.resolveForInvoke(command)
|
|
46
|
+
CallSpan.startInvoke(this.tracer, callContext, reported, connectionData, getSettings())
|
|
47
|
+
},
|
|
48
|
+
onResponse: (callContext, response) => {
|
|
49
|
+
completeSpans(callContext, response, null)
|
|
50
|
+
},
|
|
51
|
+
onInvocationError: (callContext, error) => {
|
|
52
|
+
completeSpans(callContext, null, error)
|
|
53
|
+
},
|
|
54
|
+
onProcess: (callContext, command) => {
|
|
55
|
+
const headers = {}
|
|
56
|
+
const reported = LogsCommandResolver.resolveForHandle(command, headers)
|
|
57
|
+
CallSpan.startHandle(this.tracer, callContext, reported, headers, getSettings())
|
|
58
|
+
},
|
|
59
|
+
onProcessed: (callContext, response) => {
|
|
60
|
+
completeSpans(callContext, response, null)
|
|
61
|
+
},
|
|
62
|
+
onHandlingError: (callContext, error) => {
|
|
63
|
+
completeSpans(callContext, null, error)
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const logBridge = {
|
|
68
|
+
isEnabled: (level) => level < LogsLevel.Off && level >= getSettings().logLevel,
|
|
69
|
+
|
|
70
|
+
log: (record) => {
|
|
71
|
+
const [severityNumber, severityText] = SEVERITIES[record.level] ?? SEVERITIES[LogsLevel.Info]
|
|
72
|
+
const attributes = { ...record.properties }
|
|
73
|
+
|
|
74
|
+
if (record.error !== undefined && record.error !== null) {
|
|
75
|
+
const error = record.error
|
|
76
|
+
attributes['exception.type'] = error instanceof Error ? error.name : typeof error
|
|
77
|
+
attributes['exception.message'] = String(
|
|
78
|
+
LogsRedactor.redactValue(error instanceof Error ? error.message : String(error)) ?? ''
|
|
79
|
+
)
|
|
80
|
+
if (error instanceof Error && error.stack) {
|
|
81
|
+
attributes['exception.stacktrace'] = LogsRedactor.redactBody(error.stack)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.logger.emit({
|
|
86
|
+
severityNumber,
|
|
87
|
+
severityText,
|
|
88
|
+
body: record.message,
|
|
89
|
+
attributes,
|
|
90
|
+
context: context.active(),
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const detachBroker = HypertubeBroker.attach(listener)
|
|
96
|
+
const detachLogs = HypertubeLogger.attach(logBridge)
|
|
97
|
+
this._detachFromHypertube = () => {
|
|
98
|
+
detachBroker()
|
|
99
|
+
detachLogs()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
disable() {
|
|
104
|
+
this._detachFromHypertube?.()
|
|
105
|
+
this._detachFromHypertube = undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_currentSettings() {
|
|
109
|
+
const config = this.getConfig()
|
|
110
|
+
if (config !== this._settingsSource || !this._settings) {
|
|
111
|
+
this._settingsSource = config
|
|
112
|
+
this._settings = resolveInstrumentationOptions(config)
|
|
113
|
+
}
|
|
114
|
+
return this._settings
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { LogsRedactor, commandTypeName, isCommand, runtimeNameOf } from 'hypertube-nodejs-sdk/abstractions'
|
|
2
|
+
|
|
3
|
+
const TRUNCATION_MARKER = '"__truncated__"'
|
|
4
|
+
const MAX_DEPTH = 24
|
|
5
|
+
const CLOSING_COST_PER_LEVEL = 18
|
|
6
|
+
const SCALAR_RESERVE = 32
|
|
7
|
+
|
|
8
|
+
export function renderBody(command, maxLength) {
|
|
9
|
+
if (!isCommand(command)) {
|
|
10
|
+
return undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const writer = { out: '', max: maxLength, truncated: false }
|
|
14
|
+
writeCommand(writer, command, 0)
|
|
15
|
+
return { body: LogsRedactor.redactBody(writer.out), truncated: writer.truncated }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeCommand(writer, command, depth) {
|
|
19
|
+
writer.out += `{"runtime":"${runtimeNameOf(command.runtimeName)}","type":"${commandTypeName(command.commandType)}","payload":[`
|
|
20
|
+
|
|
21
|
+
const budget = Math.max(0, writer.max - (depth + 1) * CLOSING_COST_PER_LEVEL - SCALAR_RESERVE)
|
|
22
|
+
const payload = command.payload
|
|
23
|
+
let complete = true
|
|
24
|
+
let hasContent = false
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < payload.length; i++) {
|
|
27
|
+
if (writer.out.length >= budget) {
|
|
28
|
+
writer.truncated = true
|
|
29
|
+
complete = false
|
|
30
|
+
break
|
|
31
|
+
}
|
|
32
|
+
if (hasContent) {
|
|
33
|
+
writer.out += ','
|
|
34
|
+
}
|
|
35
|
+
hasContent = true
|
|
36
|
+
|
|
37
|
+
const followsSensitiveKey =
|
|
38
|
+
i > 0 && typeof payload[i - 1] === 'string' && LogsRedactor.isSensitiveKey(payload[i - 1])
|
|
39
|
+
if (!writeValue(writer, payload[i], depth + 1, budget, followsSensitiveKey)) {
|
|
40
|
+
complete = false
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!complete) {
|
|
46
|
+
writer.out += hasContent ? `,${TRUNCATION_MARKER}` : TRUNCATION_MARKER
|
|
47
|
+
}
|
|
48
|
+
writer.out += ']}'
|
|
49
|
+
return complete
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeValue(writer, value, depth, budget, forceRedact) {
|
|
53
|
+
if (value === null || value === undefined) {
|
|
54
|
+
writer.out += 'null'
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
if (depth >= MAX_DEPTH) {
|
|
58
|
+
writer.truncated = true
|
|
59
|
+
writer.out += TRUNCATION_MARKER
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
if (isCommand(value)) {
|
|
63
|
+
return writeCommand(writer, value, depth)
|
|
64
|
+
}
|
|
65
|
+
if (forceRedact) {
|
|
66
|
+
return writeString(writer, LogsRedactor.Placeholder, budget)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (typeof value) {
|
|
70
|
+
case 'string':
|
|
71
|
+
return writeString(writer, LogsRedactor.redactValue(value), budget)
|
|
72
|
+
case 'boolean':
|
|
73
|
+
writer.out += String(value)
|
|
74
|
+
return true
|
|
75
|
+
case 'number':
|
|
76
|
+
writer.out += Number.isFinite(value) ? String(value) : `"${value}"`
|
|
77
|
+
return true
|
|
78
|
+
case 'bigint':
|
|
79
|
+
writer.out += value.toString()
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (value instanceof Uint8Array) {
|
|
84
|
+
writer.out += `"[bytes:${value.length}]"`
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
return writeString(writer, LogsRedactor.redactValue(format(value)), budget)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeString(writer, value, budget) {
|
|
91
|
+
const encoded = JSON.stringify(value)
|
|
92
|
+
if (writer.out.length + encoded.length <= budget) {
|
|
93
|
+
writer.out += encoded
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
writer.truncated = true
|
|
98
|
+
const room = Math.max(0, budget - writer.out.length - 2)
|
|
99
|
+
writer.out += JSON.stringify(value.slice(0, room))
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function format(value) {
|
|
104
|
+
try {
|
|
105
|
+
return String(value)
|
|
106
|
+
} catch {
|
|
107
|
+
return `[unrenderable:${Object.prototype.toString.call(value)}]`
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/callSpan.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { SpanKind, SpanStatusCode, TraceFlags, context, isSpanContextValid, propagation, trace } from '@opentelemetry/api'
|
|
3
|
+
import {
|
|
4
|
+
CommandType,
|
|
5
|
+
LogsRedactor,
|
|
6
|
+
commandTypeName,
|
|
7
|
+
connectionTypeName,
|
|
8
|
+
isCommand,
|
|
9
|
+
runtimeNameOf,
|
|
10
|
+
} from 'hypertube-nodejs-sdk/abstractions'
|
|
11
|
+
import { renderBody } from './bodyCapture.js'
|
|
12
|
+
import * as GraftcodeTelemetrySettings from './graftcodeTelemetrySettings.js'
|
|
13
|
+
import { describeTarget, record } from './libraryCatalog.js'
|
|
14
|
+
import { resolveOperation } from './resolveOperation.js'
|
|
15
|
+
|
|
16
|
+
export const DEPENDENCY_TYPE = 'graft.dependency.type'
|
|
17
|
+
export const DEPENDENCY_TARGET = 'graft.dependency.target'
|
|
18
|
+
export const BRIDGE_DEPENDENCY_TYPE = 'Graft'
|
|
19
|
+
export const REQUEST_BODY = 'hypertube.request.body'
|
|
20
|
+
export const RESPONSE_BODY = 'hypertube.response.body'
|
|
21
|
+
export const REQUEST_BODY_TRUNCATED = 'hypertube.request.body.truncated'
|
|
22
|
+
export const RESPONSE_BODY_TRUNCATED = 'hypertube.response.body.truncated'
|
|
23
|
+
export const TRACE_PARENT_HEADER = 'traceparent'
|
|
24
|
+
export const TRACE_STATE_HEADER = 'tracestate'
|
|
25
|
+
export const TRACE_ID_HEADER = 'traceid'
|
|
26
|
+
|
|
27
|
+
const SKIP_COMMAND_TYPES = new Set([
|
|
28
|
+
CommandType.PassByValue,
|
|
29
|
+
CommandType.RegisterForUpdate,
|
|
30
|
+
CommandType.ValueForUpdate,
|
|
31
|
+
CommandType.InvokeDelegate,
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
export class CallSpan {
|
|
35
|
+
#span
|
|
36
|
+
#settings
|
|
37
|
+
#completed = false
|
|
38
|
+
|
|
39
|
+
constructor(span, settings) {
|
|
40
|
+
this.#span = span
|
|
41
|
+
this.#settings = settings
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static startInvoke(tracer, callContext, command, connectionData, settings) {
|
|
45
|
+
const operation = tryResolveOperation(command, settings)
|
|
46
|
+
if (!operation) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
record(command, connectionData)
|
|
51
|
+
const target = describeTarget(command, connectionData, settings.libraries, operation)
|
|
52
|
+
const parent = context.active()
|
|
53
|
+
const span = tracer.startSpan(
|
|
54
|
+
`Graft ${target}`,
|
|
55
|
+
{
|
|
56
|
+
kind: SpanKind.INTERNAL,
|
|
57
|
+
attributes: invokeAttributes(operation, command, connectionData, target),
|
|
58
|
+
},
|
|
59
|
+
parent
|
|
60
|
+
)
|
|
61
|
+
const callSpan = new CallSpan(span, settings)
|
|
62
|
+
callSpan.#pushScope(callContext, parent, span)
|
|
63
|
+
callSpan.capture(command, REQUEST_BODY, REQUEST_BODY_TRUNCATED)
|
|
64
|
+
|
|
65
|
+
if (GraftcodeTelemetrySettings.isAutoInjectEnabled() && !hasOwnTraceparent(connectionData)) {
|
|
66
|
+
inject(trace.setSpan(parent, span), callContext.outgoingHeaders)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return callSpan
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static startHandle(tracer, callContext, command, headers, settings) {
|
|
73
|
+
const operation = tryResolveOperation(command, settings)
|
|
74
|
+
if (!operation) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const parent = extractParent(headers)
|
|
79
|
+
const span = tracer.startSpan(
|
|
80
|
+
operation.name,
|
|
81
|
+
{ kind: SpanKind.INTERNAL, attributes: handleAttributes(operation, command) },
|
|
82
|
+
parent
|
|
83
|
+
)
|
|
84
|
+
const callSpan = new CallSpan(span, settings)
|
|
85
|
+
callSpan.#pushScope(callContext, parent, span)
|
|
86
|
+
callSpan.capture(command, REQUEST_BODY, REQUEST_BODY_TRUNCATED)
|
|
87
|
+
return callSpan
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
complete(response, error) {
|
|
91
|
+
if (this.#completed) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
this.#completed = true
|
|
95
|
+
|
|
96
|
+
if (response != null) {
|
|
97
|
+
this.capture(response, RESPONSE_BODY, RESPONSE_BODY_TRUNCATED)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (error == null) {
|
|
101
|
+
if (isCommand(response) && response.commandType === CommandType.Exception) {
|
|
102
|
+
const name = typeof response.payload[2] === 'string' ? response.payload[2] : 'Exception'
|
|
103
|
+
const message = typeof response.payload[3] === 'string' ? response.payload[3] : ''
|
|
104
|
+
markFailed(this.#span, name, message)
|
|
105
|
+
} else {
|
|
106
|
+
this.#span.setStatus({ code: SpanStatusCode.OK })
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
const name = error instanceof Error ? error.name : typeof error
|
|
110
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
111
|
+
const stack =
|
|
112
|
+
error instanceof Error && error.stack ? LogsRedactor.redactBody(error.stack) : undefined
|
|
113
|
+
markFailed(this.#span, name, message, stack)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.#span.end()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
capture(command, bodyKey, truncatedKey) {
|
|
120
|
+
if (!this.#span.isRecording() || !GraftcodeTelemetrySettings.isBodyCaptureEnabled()) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
const rendered = renderBody(command, GraftcodeTelemetrySettings.getBodyMaxCharacters())
|
|
124
|
+
if (!rendered) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
this.#span.setAttribute(bodyKey, rendered.body)
|
|
128
|
+
if (rendered.truncated) {
|
|
129
|
+
this.#span.setAttribute(truncatedKey, true)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#pushScope(callContext, parent, span) {
|
|
134
|
+
const active = trace.setSpan(parent, span)
|
|
135
|
+
callContext.items.push(this)
|
|
136
|
+
callContext.items.push({
|
|
137
|
+
wrap(run) {
|
|
138
|
+
return context.with(active, run)
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function tryResolveOperation(command, settings) {
|
|
145
|
+
if (!isCommand(command) || SKIP_COMMAND_TYPES.has(command.commandType)) {
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const operation = resolveOperation(command)
|
|
150
|
+
if (!operation || settings.ignore?.(operation)) {
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (operation.name.toLowerCase().endsWith('telemetrysetup.initialize')) {
|
|
155
|
+
return undefined
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return operation
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function invokeAttributes(operation, command, connectionData, target) {
|
|
162
|
+
const attributes = handleAttributes(operation, command)
|
|
163
|
+
const connectionType = safe(() => connectionData.connectionType)
|
|
164
|
+
|
|
165
|
+
attributes[DEPENDENCY_TYPE] = BRIDGE_DEPENDENCY_TYPE
|
|
166
|
+
attributes[DEPENDENCY_TARGET] = String(LogsRedactor.redactValue(target) ?? '')
|
|
167
|
+
attributes['hypertube.target'] = safe(() => LogsRedactor.redactTarget(connectionData.hostname)) ?? ''
|
|
168
|
+
if (typeof connectionType === 'number') {
|
|
169
|
+
attributes['hypertube.connection_type'] = connectionTypeName(connectionType)
|
|
170
|
+
}
|
|
171
|
+
return attributes
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function handleAttributes(operation, command) {
|
|
175
|
+
return {
|
|
176
|
+
'hypertube.runtime': runtimeNameOf(command.runtimeName),
|
|
177
|
+
'hypertube.command_type': commandTypeName(command.commandType),
|
|
178
|
+
'hypertube.operation': operation.name,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function markFailed(span, name, message, stack) {
|
|
183
|
+
const redacted = String(LogsRedactor.redactValue(message) ?? '')
|
|
184
|
+
span.recordException({ name, message: redacted, stack })
|
|
185
|
+
span.setAttribute('exception.type', name)
|
|
186
|
+
if (redacted) {
|
|
187
|
+
span.setAttribute('exception.message', redacted)
|
|
188
|
+
}
|
|
189
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: redacted })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function inject(active, headers) {
|
|
193
|
+
propagation.inject(active, headers)
|
|
194
|
+
const spanContext = trace.getSpanContext(active)
|
|
195
|
+
if (spanContext && isSpanContextValid(spanContext)) {
|
|
196
|
+
headers[TRACE_ID_HEADER] = spanContext.traceId
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function extractParent(headers) {
|
|
201
|
+
const base = context.active()
|
|
202
|
+
const extracted = propagation.extract(base, headers)
|
|
203
|
+
const extractedSpan = trace.getSpanContext(extracted)
|
|
204
|
+
if (extractedSpan && isSpanContextValid(extractedSpan) && extractedSpan !== trace.getSpanContext(base)) {
|
|
205
|
+
return extracted
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const traceId = readTraceId(headers[TRACE_ID_HEADER])
|
|
209
|
+
if (!traceId) {
|
|
210
|
+
return base
|
|
211
|
+
}
|
|
212
|
+
return trace.setSpanContext(base, {
|
|
213
|
+
traceId,
|
|
214
|
+
spanId: randomSpanId(),
|
|
215
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
216
|
+
isRemote: true,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readTraceId(raw) {
|
|
221
|
+
const hex = raw?.trim().replaceAll('-', '').toLowerCase()
|
|
222
|
+
return hex && /^[0-9a-f]{32}$/.test(hex) && !/^0+$/.test(hex) ? hex : undefined
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function randomSpanId() {
|
|
226
|
+
let spanId
|
|
227
|
+
do {
|
|
228
|
+
spanId = randomBytes(8).toString('hex')
|
|
229
|
+
} while (/^0+$/.test(spanId))
|
|
230
|
+
return spanId
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasOwnTraceparent(connectionData) {
|
|
234
|
+
try {
|
|
235
|
+
return TRACE_PARENT_HEADER in connectionData.getHeaders()
|
|
236
|
+
} catch {
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function safe(read) {
|
|
242
|
+
try {
|
|
243
|
+
return read()
|
|
244
|
+
} catch {
|
|
245
|
+
return undefined
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function completeSpans(callContext, response, error) {
|
|
250
|
+
for (let i = callContext.items.length - 1; i >= 0; i--) {
|
|
251
|
+
const item = callContext.items[i]
|
|
252
|
+
if (item instanceof CallSpan) {
|
|
253
|
+
item.complete(response, error)
|
|
254
|
+
callContext.items.splice(i, 1)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { LogsLevel, parseLogsLevel } from 'hypertube-nodejs-sdk/abstractions'
|
|
2
|
+
|
|
3
|
+
export const MINIMUM_BODY_MAX_CHARS = 256
|
|
4
|
+
export const DEFAULT_BODY_MAX_CHARS = 8192
|
|
5
|
+
export const MAXIMUM_BODY_MAX_CHARS = 32768
|
|
6
|
+
|
|
7
|
+
let autoInject = readFlag('HYPERTUBE_TELEMETRY_AUTO_INJECT', true)
|
|
8
|
+
let captureBodies = readFlag('HYPERTUBE_TELEMETRY_CAPTURE_BODIES', false)
|
|
9
|
+
let bodyMaxCharacters = readBodyMaxChars()
|
|
10
|
+
|
|
11
|
+
export function isAutoInjectEnabled() {
|
|
12
|
+
return autoInject
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function setAutoInjectEnabled(value) {
|
|
16
|
+
autoInject = value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isBodyCaptureEnabled() {
|
|
20
|
+
return captureBodies
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function setBodyCaptureEnabled(value) {
|
|
24
|
+
captureBodies = value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getBodyMaxCharacters() {
|
|
28
|
+
return bodyMaxCharacters
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setBodyMaxCharacters(value) {
|
|
32
|
+
bodyMaxCharacters = clampBodyMaxCharacters(value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function clampBodyMaxCharacters(value) {
|
|
36
|
+
if (value < MINIMUM_BODY_MAX_CHARS) {
|
|
37
|
+
return MINIMUM_BODY_MAX_CHARS
|
|
38
|
+
}
|
|
39
|
+
return Math.min(value, MAXIMUM_BODY_MAX_CHARS)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function applyInstrumentationConfig(config) {
|
|
43
|
+
if (config.propagateContext !== undefined) {
|
|
44
|
+
setAutoInjectEnabled(config.propagateContext)
|
|
45
|
+
}
|
|
46
|
+
if (config.captureBodies !== undefined) {
|
|
47
|
+
setBodyCaptureEnabled(config.captureBodies)
|
|
48
|
+
}
|
|
49
|
+
if (config.maxBodyLength !== undefined) {
|
|
50
|
+
setBodyMaxCharacters(config.maxBodyLength)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveInstrumentationOptions(config) {
|
|
55
|
+
applyInstrumentationConfig(config)
|
|
56
|
+
return {
|
|
57
|
+
logLevel: parseLogsLevel(config.logLevel ?? 'info') ?? LogsLevel.Info,
|
|
58
|
+
ignore: typeof config.ignore === 'function' ? config.ignore : undefined,
|
|
59
|
+
libraries:
|
|
60
|
+
typeof config.libraries === 'function' || (config.libraries && typeof config.libraries === 'object')
|
|
61
|
+
? config.libraries
|
|
62
|
+
: undefined,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readBodyMaxChars() {
|
|
67
|
+
const value = Number.parseInt(process.env.HYPERTUBE_TELEMETRY_BODY_MAX_CHARS ?? '', 10)
|
|
68
|
+
return Number.isFinite(value) ? clampBodyMaxCharacters(value) : DEFAULT_BODY_MAX_CHARS
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readFlag(name, defaultValue) {
|
|
72
|
+
const value = process.env[name]?.trim().toLowerCase()
|
|
73
|
+
if (!value) {
|
|
74
|
+
return defaultValue
|
|
75
|
+
}
|
|
76
|
+
return value !== 'false' && value !== '0'
|
|
77
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { InstrumentationBase, InstrumentationConfig } from '@opentelemetry/instrumentation'
|
|
2
|
+
|
|
3
|
+
export interface GraftOperation {
|
|
4
|
+
name: string
|
|
5
|
+
service?: string
|
|
6
|
+
method: string
|
|
7
|
+
commandType: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LibraryTarget {
|
|
11
|
+
target: string
|
|
12
|
+
channel: string
|
|
13
|
+
runtime: string
|
|
14
|
+
service?: string
|
|
15
|
+
method: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type LibraryResolver = Record<string, string> | ((target: LibraryTarget) => string | undefined)
|
|
19
|
+
|
|
20
|
+
export interface GraftcodeInstrumentationConfig extends InstrumentationConfig {
|
|
21
|
+
captureBodies?: boolean
|
|
22
|
+
maxBodyLength?: number
|
|
23
|
+
propagateContext?: boolean
|
|
24
|
+
logLevel?: 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'critical' | 'off'
|
|
25
|
+
ignore?: (operation: GraftOperation) => boolean
|
|
26
|
+
libraries?: LibraryResolver
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare const INSTRUMENTATION_NAME: 'Graftcode'
|
|
30
|
+
export declare const LOGGER_NAME: 'Hypertube'
|
|
31
|
+
|
|
32
|
+
export declare class GraftcodeInstrumentation extends InstrumentationBase<GraftcodeInstrumentationConfig> {
|
|
33
|
+
constructor(config?: GraftcodeInstrumentationConfig)
|
|
34
|
+
protected init(): []
|
|
35
|
+
enable(): void
|
|
36
|
+
disable(): void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export declare function resolveOperation(command: unknown): GraftOperation | undefined
|
package/src/index.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { basename, dirname, extname, join } from 'node:path'
|
|
3
|
+
import { CommandType, connectionTypeName, isCommand, runtimeNameOf } from 'hypertube-nodejs-sdk/abstractions'
|
|
4
|
+
import { resolveTargetTypeName } from './operationName.js'
|
|
5
|
+
|
|
6
|
+
const MAX_DEPTH = 32
|
|
7
|
+
const ENTRY_FILE_NAMES = new Set(['index', 'main'])
|
|
8
|
+
|
|
9
|
+
const librariesByConnection = new Map()
|
|
10
|
+
const versionByPath = new Map()
|
|
11
|
+
|
|
12
|
+
export function record(command, connectionData) {
|
|
13
|
+
for (const load of findCommands(command, CommandType.LoadLibrary, 0)) {
|
|
14
|
+
const path = libraryPath(load)
|
|
15
|
+
const name = path ? libraryNameFromPath(path) : undefined
|
|
16
|
+
if (!path || !name) {
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const key = connectionKey(connectionData)
|
|
21
|
+
let libraries = librariesByConnection.get(key)
|
|
22
|
+
if (!libraries) {
|
|
23
|
+
libraries = new Map()
|
|
24
|
+
librariesByConnection.set(key, libraries)
|
|
25
|
+
}
|
|
26
|
+
libraries.set(name.toLowerCase(), { name, version: readVersion(path) })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function describeTarget(command, connectionData, configured, operation) {
|
|
31
|
+
const channel = channelName(connectionData)
|
|
32
|
+
const library = resolveLibrary(command, connectionData, operation, configured)
|
|
33
|
+
if (!library) {
|
|
34
|
+
return `${runtimeNameOf(command.runtimeName)} ${channel}`
|
|
35
|
+
}
|
|
36
|
+
return library.version ? `${library.name} ${library.version} ${channel}` : `${library.name} ${channel}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function channelName(connectionData) {
|
|
40
|
+
return typeof connectionData.connectionType === 'number'
|
|
41
|
+
? connectionTypeName(connectionData.connectionType).toLowerCase()
|
|
42
|
+
: 'unknown'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resetLibraryCatalog() {
|
|
46
|
+
librariesByConnection.clear()
|
|
47
|
+
versionByPath.clear()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveLibrary(command, connectionData, operation, configured) {
|
|
51
|
+
const named = configuredName(configured, connectionData, command, operation)
|
|
52
|
+
if (named) {
|
|
53
|
+
return lookup(connectionData, named) ?? { name: named, version: '' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const load of findCommands(command, CommandType.LoadLibrary, 0)) {
|
|
57
|
+
const path = libraryPath(load)
|
|
58
|
+
const name = path ? libraryNameFromPath(path) : undefined
|
|
59
|
+
if (name) {
|
|
60
|
+
return lookup(connectionData, name) ?? { name, version: '' }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const libraries = librariesByConnection.get(connectionKey(connectionData))
|
|
65
|
+
const typeName = operation?.service ?? resolveTargetTypeName(command)
|
|
66
|
+
if (!typeName) {
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (libraries) {
|
|
71
|
+
let best
|
|
72
|
+
for (const library of libraries.values()) {
|
|
73
|
+
if (isTypeOfLibrary(typeName, library.name) && (!best || library.name.length > best.name.length)) {
|
|
74
|
+
best = library
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (best) {
|
|
78
|
+
return best
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const separator = typeName.indexOf('.')
|
|
83
|
+
if (separator > 0) {
|
|
84
|
+
return { name: typeName.slice(0, separator), version: '' }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (libraries?.size === 1) {
|
|
88
|
+
return libraries.values().next().value
|
|
89
|
+
}
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function configuredName(configured, connectionData, command, operation) {
|
|
94
|
+
if (!configured) {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const target = connectionData.hostname ?? ''
|
|
99
|
+
let name
|
|
100
|
+
try {
|
|
101
|
+
name =
|
|
102
|
+
typeof configured === 'function'
|
|
103
|
+
? configured({
|
|
104
|
+
target,
|
|
105
|
+
channel: channelName(connectionData),
|
|
106
|
+
runtime: runtimeNameOf(command.runtimeName),
|
|
107
|
+
service: operation?.service ?? resolveTargetTypeName(command),
|
|
108
|
+
method: operation?.method,
|
|
109
|
+
})
|
|
110
|
+
: (configured[target] ?? configured[target.replace(/\/+$/, '')])
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
return typeof name === 'string' && name.trim() ? name.trim() : undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function lookup(connectionData, name) {
|
|
118
|
+
return librariesByConnection.get(connectionKey(connectionData))?.get(name.toLowerCase())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isTypeOfLibrary(typeName, libraryName) {
|
|
122
|
+
const type = typeName.toLowerCase()
|
|
123
|
+
const library = libraryName.toLowerCase()
|
|
124
|
+
return type === library || type.startsWith(`${library}.`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function connectionKey(connectionData) {
|
|
128
|
+
return `${connectionData.connectionType}|${connectionData.hostname ?? ''}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findCommands(command, commandType, depth) {
|
|
132
|
+
if (depth > MAX_DEPTH || !isCommand(command)) {
|
|
133
|
+
return []
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const matches = command.commandType === commandType ? [command] : []
|
|
137
|
+
for (const item of command.payload) {
|
|
138
|
+
matches.push(...findCommands(item, commandType, depth + 1))
|
|
139
|
+
}
|
|
140
|
+
return matches
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function libraryPath(load) {
|
|
144
|
+
for (let i = load.payload.length - 1; i >= 0; i--) {
|
|
145
|
+
const item = load.payload[i]
|
|
146
|
+
const text = isCommand(item) && item.commandType === CommandType.Value ? item.payload[0] : item
|
|
147
|
+
if (typeof text === 'string' && text.trim()) {
|
|
148
|
+
return text.trim()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function libraryNameFromPath(path) {
|
|
155
|
+
const trimmed = path.replace(/[\\/]+$/, '')
|
|
156
|
+
const file = basename(trimmed)
|
|
157
|
+
if (file.toLowerCase() === 'package.json') {
|
|
158
|
+
return basename(dirname(trimmed)) || undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const name = file.slice(0, file.length - extname(file).length) || file
|
|
162
|
+
if (ENTRY_FILE_NAMES.has(name.toLowerCase()) && /\.[cm]?js$/i.test(file)) {
|
|
163
|
+
return basename(dirname(trimmed)) || name
|
|
164
|
+
}
|
|
165
|
+
return name || undefined
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readVersion(path) {
|
|
169
|
+
const cached = versionByPath.get(path)
|
|
170
|
+
if (cached !== undefined) {
|
|
171
|
+
return cached
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let version = ''
|
|
175
|
+
try {
|
|
176
|
+
const trimmed = path.replace(/[\\/]+$/, '')
|
|
177
|
+
const packageJson = trimmed.toLowerCase().endsWith('package.json')
|
|
178
|
+
? trimmed
|
|
179
|
+
: statSync(trimmed).isDirectory()
|
|
180
|
+
? join(trimmed, 'package.json')
|
|
181
|
+
: join(dirname(trimmed), 'package.json')
|
|
182
|
+
const parsed = JSON.parse(readFileSync(packageJson, 'utf8'))
|
|
183
|
+
version = typeof parsed.version === 'string' ? parsed.version.trim() : ''
|
|
184
|
+
} catch {
|
|
185
|
+
version = ''
|
|
186
|
+
}
|
|
187
|
+
versionByPath.set(path, version)
|
|
188
|
+
return version
|
|
189
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { CommandType, isCommand } from 'hypertube-nodejs-sdk/abstractions'
|
|
2
|
+
|
|
3
|
+
const MAX_DEPTH = 32
|
|
4
|
+
|
|
5
|
+
export function isBlank(value) {
|
|
6
|
+
return value == null || String(value).trim() === ''
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolve(command) {
|
|
10
|
+
if (!isCommand(command)) {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
switch (command.commandType) {
|
|
15
|
+
case CommandType.InvokeStaticMethod:
|
|
16
|
+
case CommandType.InvokeInstanceMethod:
|
|
17
|
+
case CommandType.InvokeGenericStaticMethod:
|
|
18
|
+
case CommandType.InvokeGenericMethod:
|
|
19
|
+
case CommandType.GetStaticField:
|
|
20
|
+
case CommandType.SetStaticField:
|
|
21
|
+
case CommandType.GetInstanceField:
|
|
22
|
+
case CommandType.SetInstanceField:
|
|
23
|
+
case CommandType.GetStaticMethodAsDelegate:
|
|
24
|
+
case CommandType.GetInstanceMethodAsDelegate: {
|
|
25
|
+
const qualified = qualify(
|
|
26
|
+
resolveTypeNameCore(command.payload[0], 0),
|
|
27
|
+
resolveText(command.payload[1], 0)
|
|
28
|
+
)
|
|
29
|
+
return qualified ?? fallback(command)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
case CommandType.CreateClassInstance: {
|
|
33
|
+
const createdType = resolveTypeNameCore(command.payload[0], 0)
|
|
34
|
+
return createdType == null ? fallback(command) : `${createdType}.ctor`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
case CommandType.InvokeGlobalFunction:
|
|
38
|
+
case CommandType.GetGlobalField: {
|
|
39
|
+
const name = resolveText(command.payload[0], 0)
|
|
40
|
+
return name ?? fallback(command)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
default:
|
|
44
|
+
return fallback(command)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveTargetTypeName(command) {
|
|
49
|
+
if (!isCommand(command)) {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
switch (command.commandType) {
|
|
54
|
+
case CommandType.InvokeStaticMethod:
|
|
55
|
+
case CommandType.InvokeInstanceMethod:
|
|
56
|
+
case CommandType.InvokeGenericStaticMethod:
|
|
57
|
+
case CommandType.InvokeGenericMethod:
|
|
58
|
+
case CommandType.GetStaticField:
|
|
59
|
+
case CommandType.SetStaticField:
|
|
60
|
+
case CommandType.GetInstanceField:
|
|
61
|
+
case CommandType.SetInstanceField:
|
|
62
|
+
case CommandType.GetStaticMethodAsDelegate:
|
|
63
|
+
case CommandType.GetInstanceMethodAsDelegate:
|
|
64
|
+
case CommandType.CreateClassInstance:
|
|
65
|
+
return resolveTypeNameCore(command.payload[0], 0)
|
|
66
|
+
|
|
67
|
+
case CommandType.GetType:
|
|
68
|
+
return resolveTypeNameCore(command, 0)
|
|
69
|
+
|
|
70
|
+
default:
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function fallback(command) {
|
|
76
|
+
return `hypertube.${command.commandType}`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function qualify(typeName, memberName) {
|
|
80
|
+
if (isBlank(memberName)) {
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
return isBlank(typeName) ? memberName : `${typeName}.${memberName}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveText(payloadItem, depth) {
|
|
87
|
+
if (depth > MAX_DEPTH) {
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
if (typeof payloadItem === 'string') {
|
|
91
|
+
return payloadItem.trim() || undefined
|
|
92
|
+
}
|
|
93
|
+
if (isCommand(payloadItem) && payloadItem.commandType === CommandType.Value) {
|
|
94
|
+
return resolveText(payloadItem.payload[0], depth + 1)
|
|
95
|
+
}
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolveTypeNameCore(target, depth) {
|
|
100
|
+
if (depth > MAX_DEPTH || target == null) {
|
|
101
|
+
return undefined
|
|
102
|
+
}
|
|
103
|
+
if (typeof target === 'string') {
|
|
104
|
+
return target.trim() || undefined
|
|
105
|
+
}
|
|
106
|
+
if (!isCommand(target)) {
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
return resolveTypeNameFromCommand(target, depth)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveTypeNameFromCommand(command, depth) {
|
|
113
|
+
switch (command.commandType) {
|
|
114
|
+
case CommandType.Value:
|
|
115
|
+
return resolveTypeNameCore(command.payload[0], depth + 1)
|
|
116
|
+
|
|
117
|
+
case CommandType.GetType:
|
|
118
|
+
case CommandType.Cast:
|
|
119
|
+
case CommandType.CreateNull:
|
|
120
|
+
for (const item of command.payload) {
|
|
121
|
+
const name = resolveText(item, depth + 1)
|
|
122
|
+
if (!isBlank(name)) {
|
|
123
|
+
return name
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined
|
|
127
|
+
|
|
128
|
+
case CommandType.CreateClassInstance:
|
|
129
|
+
case CommandType.SetInstanceField:
|
|
130
|
+
case CommandType.InvokeStaticMethod:
|
|
131
|
+
case CommandType.InvokeInstanceMethod:
|
|
132
|
+
case CommandType.GetStaticField:
|
|
133
|
+
case CommandType.GetInstanceField:
|
|
134
|
+
return resolveTypeNameCore(command.payload[0], depth + 1)
|
|
135
|
+
|
|
136
|
+
default:
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { CommandType, LogsRedactor, commandTypeName, isCommand } from 'hypertube-nodejs-sdk/abstractions'
|
|
2
|
+
|
|
3
|
+
const MAX_DEPTH = 32
|
|
4
|
+
|
|
5
|
+
const MEMBER_COMMANDS = new Set([
|
|
6
|
+
CommandType.InvokeStaticMethod,
|
|
7
|
+
CommandType.InvokeInstanceMethod,
|
|
8
|
+
CommandType.InvokeGenericStaticMethod,
|
|
9
|
+
CommandType.InvokeGenericMethod,
|
|
10
|
+
CommandType.GetStaticField,
|
|
11
|
+
CommandType.SetStaticField,
|
|
12
|
+
CommandType.GetInstanceField,
|
|
13
|
+
CommandType.SetInstanceField,
|
|
14
|
+
CommandType.GetStaticMethodAsDelegate,
|
|
15
|
+
CommandType.GetInstanceMethodAsDelegate,
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
const TYPE_FROM_TARGET_COMMANDS = new Set([
|
|
19
|
+
CommandType.CreateClassInstance,
|
|
20
|
+
CommandType.SetInstanceField,
|
|
21
|
+
CommandType.InvokeStaticMethod,
|
|
22
|
+
CommandType.InvokeInstanceMethod,
|
|
23
|
+
CommandType.GetStaticField,
|
|
24
|
+
CommandType.GetInstanceField,
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
export function resolveOperation(command) {
|
|
28
|
+
if (!isCommand(command)) {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let service
|
|
33
|
+
let method
|
|
34
|
+
|
|
35
|
+
if (MEMBER_COMMANDS.has(command.commandType)) {
|
|
36
|
+
service = resolveTypeName(command.payload[0], 0)
|
|
37
|
+
method = resolveText(command.payload[1], 0)
|
|
38
|
+
} else if (command.commandType === CommandType.CreateClassInstance) {
|
|
39
|
+
service = resolveTypeName(command.payload[0], 0)
|
|
40
|
+
method = service ? 'ctor' : undefined
|
|
41
|
+
} else if (
|
|
42
|
+
command.commandType === CommandType.InvokeGlobalFunction ||
|
|
43
|
+
command.commandType === CommandType.GetGlobalField
|
|
44
|
+
) {
|
|
45
|
+
method = resolveText(command.payload[0], 0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!method) {
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
service = service ? LogsRedactor.redactValue(service) : undefined
|
|
53
|
+
method = LogsRedactor.redactValue(method)
|
|
54
|
+
return {
|
|
55
|
+
name: service ? `${service}.${method}` : method,
|
|
56
|
+
service,
|
|
57
|
+
method,
|
|
58
|
+
commandType: commandTypeName(command.commandType),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveText(item, depth) {
|
|
63
|
+
if (depth > MAX_DEPTH) {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
if (typeof item === 'string') {
|
|
67
|
+
return item.trim() || undefined
|
|
68
|
+
}
|
|
69
|
+
if (isCommand(item) && item.commandType === CommandType.Value) {
|
|
70
|
+
return resolveText(item.payload[0], depth + 1)
|
|
71
|
+
}
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveTypeName(target, depth) {
|
|
76
|
+
if (depth > MAX_DEPTH || target == null) {
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
if (typeof target === 'string') {
|
|
80
|
+
return target.trim() || undefined
|
|
81
|
+
}
|
|
82
|
+
if (!isCommand(target)) {
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
switch (target.commandType) {
|
|
87
|
+
case CommandType.Value:
|
|
88
|
+
return resolveTypeName(target.payload[0], depth + 1)
|
|
89
|
+
case CommandType.GetType:
|
|
90
|
+
case CommandType.Cast:
|
|
91
|
+
case CommandType.CreateNull:
|
|
92
|
+
for (const item of target.payload) {
|
|
93
|
+
const name = resolveText(item, depth + 1)
|
|
94
|
+
if (name) {
|
|
95
|
+
return name
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return undefined
|
|
99
|
+
default:
|
|
100
|
+
return TYPE_FROM_TARGET_COMMANDS.has(target.commandType)
|
|
101
|
+
? resolveTypeName(target.payload[0], depth + 1)
|
|
102
|
+
: undefined
|
|
103
|
+
}
|
|
104
|
+
}
|