Skip to content

Commit 5743779

Browse files
Merge pull request #454 from solarwinds/NH-78538
Move AppOptics reporter and span exporter to main package
2 parents 5dba45e + eadafd9 commit 5743779

File tree

8 files changed

+474
-167
lines changed

8 files changed

+474
-167
lines changed

.yarn/versions/9552d4be.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
releases:
2+
solarwinds-apm: minor
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
Copyright 2023-2024 SolarWinds Worldwide, LLC.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { inspect } from "node:util"
18+
19+
import {
20+
type AttributeValue,
21+
type DiagLogger,
22+
type SpanContext,
23+
SpanKind,
24+
SpanStatusCode,
25+
} from "@opentelemetry/api"
26+
import {
27+
type ExportResult,
28+
ExportResultCode,
29+
hrTimeToMicroseconds,
30+
} from "@opentelemetry/core"
31+
import {
32+
type ReadableSpan,
33+
type SpanExporter,
34+
type TimedEvent,
35+
} from "@opentelemetry/sdk-trace-base"
36+
import {
37+
ATTR_EXCEPTION_MESSAGE,
38+
ATTR_EXCEPTION_STACKTRACE,
39+
ATTR_EXCEPTION_TYPE,
40+
} from "@opentelemetry/semantic-conventions"
41+
import { oboe } from "@solarwinds-apm/bindings"
42+
43+
import { TRANSACTION_NAME_ATTRIBUTE } from "../../processing/transaction-name.js"
44+
import { traceParent } from "../sampler.js"
45+
46+
export class AppopticsTraceExporter implements SpanExporter {
47+
readonly #reporter: oboe.Reporter
48+
#error: Error | undefined = undefined
49+
50+
constructor(
51+
reporter: oboe.Reporter,
52+
protected readonly logger: DiagLogger,
53+
) {
54+
this.#reporter = reporter
55+
}
56+
57+
export(
58+
spans: ReadableSpan[],
59+
resultCallback: (result: ExportResult) => void,
60+
) {
61+
for (const span of spans) {
62+
const context = span.spanContext()
63+
const md = this.#metadata(context)
64+
65+
let evt: oboe.Event
66+
if (span.parentSpanId) {
67+
evt = oboe.Context.createEntry(
68+
md,
69+
hrTimeToMicroseconds(span.startTime),
70+
this.#metadata({
71+
traceFlags: context.traceFlags,
72+
traceId: context.traceId,
73+
spanId: span.parentSpanId,
74+
}),
75+
)
76+
} else {
77+
evt = oboe.Context.createEntry(md, hrTimeToMicroseconds(span.startTime))
78+
}
79+
80+
const kind = SpanKind[span.kind]
81+
const layer = `${kind}:${span.name}`
82+
83+
evt.addInfo("Layer", layer)
84+
evt.addInfo("sw.span_name", span.name)
85+
evt.addInfo("sw.span_kind", kind)
86+
evt.addInfo("Language", "Node.js")
87+
88+
evt.addInfo("otel.scope.name", span.instrumentationLibrary.name)
89+
evt.addInfo(
90+
"otel.scope.version",
91+
span.instrumentationLibrary.version ?? null,
92+
)
93+
if (span.status.code !== SpanStatusCode.UNSET) {
94+
evt.addInfo("otel.status_code", SpanStatusCode[span.status.code])
95+
}
96+
if (span.status.message) {
97+
evt.addInfo("otel.status_description", span.status.message)
98+
}
99+
if (span.droppedAttributesCount > 0) {
100+
evt.addInfo(
101+
"otel.dropped_attributes_count",
102+
span.droppedAttributesCount,
103+
)
104+
}
105+
if (span.droppedEventsCount > 0) {
106+
evt.addInfo("otel.dropped_events_count", span.droppedEventsCount)
107+
}
108+
if (span.droppedLinksCount > 0) {
109+
evt.addInfo("otel.dropped_links_count", span.droppedLinksCount)
110+
}
111+
112+
const rename: Record<string, string> = {
113+
[TRANSACTION_NAME_ATTRIBUTE]: "TransactionName",
114+
}
115+
for (const [key, value] of Object.entries(span.attributes)) {
116+
const name = rename[key] ?? key
117+
evt.addInfo(name, this.#attributeValue(value))
118+
}
119+
120+
this.#sendReport(evt)
121+
122+
for (const event of span.events) {
123+
if (event.name === "exception") {
124+
this.#reportErrorEvent(event)
125+
} else {
126+
this.#reportInfoEvent(event)
127+
}
128+
}
129+
130+
evt = oboe.Context.createExit(hrTimeToMicroseconds(span.endTime))
131+
evt.addInfo("Layer", layer)
132+
this.#sendReport(evt)
133+
}
134+
135+
const result: ExportResult = this.#error
136+
? { code: ExportResultCode.FAILED, error: this.#error }
137+
: { code: ExportResultCode.SUCCESS }
138+
this.#error = undefined
139+
resultCallback(result)
140+
}
141+
142+
forceFlush(): Promise<void> {
143+
this.#reporter.flush()
144+
return Promise.resolve()
145+
}
146+
shutdown(): Promise<void> {
147+
oboe.Context.shutdown()
148+
return Promise.resolve()
149+
}
150+
151+
#metadata(spanContext: SpanContext): oboe.Metadata {
152+
return oboe.Metadata.fromString(traceParent(spanContext))
153+
}
154+
155+
#attributeValue(
156+
v: AttributeValue | undefined,
157+
): string | number | boolean | null {
158+
if (Array.isArray(v)) {
159+
return inspect(v, { breakLength: Infinity, compact: true })
160+
} else {
161+
return v ?? null
162+
}
163+
}
164+
165+
#reportErrorEvent(event: TimedEvent) {
166+
const evt = oboe.Context.createEvent(hrTimeToMicroseconds(event.time))
167+
evt.addInfo("Label", "error")
168+
evt.addInfo("Spec", "error")
169+
170+
const rename: Record<string, string> = {
171+
[ATTR_EXCEPTION_TYPE]: "ErrorClass",
172+
[ATTR_EXCEPTION_MESSAGE]: "ErrorMsg",
173+
[ATTR_EXCEPTION_STACKTRACE]: "Backtrace",
174+
}
175+
for (const [key, value] of Object.entries(event.attributes ?? {})) {
176+
const name = rename[key] ?? key
177+
evt.addInfo(name, this.#attributeValue(value))
178+
}
179+
180+
this.#sendReport(evt)
181+
}
182+
183+
#reportInfoEvent(event: TimedEvent) {
184+
const evt = oboe.Context.createEvent(hrTimeToMicroseconds(event.time))
185+
evt.addInfo("Label", "info")
186+
for (const [key, value] of Object.entries(event.attributes ?? {})) {
187+
evt.addInfo(key, this.#attributeValue(value))
188+
}
189+
this.#sendReport(evt)
190+
}
191+
192+
#sendReport(evt: oboe.Event): void {
193+
const status = this.#reporter.sendReport(evt, false)
194+
if (status < 0) {
195+
this.#error = new Error(
196+
`Reporter::sendReport returned with error status ${status}`,
197+
)
198+
this.logger.warn("error sending report", this.#error)
199+
this.logger.debug(evt.metadataString())
200+
}
201+
}
202+
}

packages/solarwinds-apm/src/appoptics/processing/inbound-metrics.ts

Lines changed: 8 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,13 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { type DiagLogger, SpanKind, SpanStatusCode } from "@opentelemetry/api"
17+
import { type DiagLogger, SpanStatusCode } from "@opentelemetry/api"
1818
import { hrTimeToMicroseconds } from "@opentelemetry/core"
1919
import {
2020
NoopSpanProcessor,
2121
type ReadableSpan,
2222
type SpanProcessor,
2323
} from "@opentelemetry/sdk-trace-base"
24-
import {
25-
ATTR_HTTP_REQUEST_METHOD,
26-
ATTR_HTTP_RESPONSE_STATUS_CODE,
27-
ATTR_URL_FULL,
28-
} from "@opentelemetry/semantic-conventions"
2924
import { oboe } from "@solarwinds-apm/bindings"
3025
import { type SwConfiguration } from "@solarwinds-apm/sdk"
3126

@@ -34,11 +29,7 @@ import {
3429
computedTransactionName,
3530
TRANSACTION_NAME_ATTRIBUTE,
3631
} from "../../processing/transaction-name.js"
37-
import {
38-
ATTR_HTTP_METHOD,
39-
ATTR_HTTP_STATUS_CODE,
40-
ATTR_HTTP_URL,
41-
} from "../../semattrs.old.js"
32+
import { httpSpanMetadata } from "../../sampling/sampler.js"
4233

4334
export class AppopticsInboundMetricsProcessor
4435
extends NoopSpanProcessor
@@ -59,7 +50,7 @@ export class AppopticsInboundMetricsProcessor
5950
return
6051
}
6152

62-
const { isHttp, method, status, url } = httpSpanMetadata(span)
53+
const meta = httpSpanMetadata(span.kind, span.attributes)
6354
const has_error = span.status.code === SpanStatusCode.ERROR ? 1 : 0
6455
const duration = hrTimeToMicroseconds(span.duration)
6556

@@ -70,15 +61,15 @@ export class AppopticsInboundMetricsProcessor
7061
this.#defaultTransactionName ?? computedTransactionName(span)
7162
}
7263

73-
if (isHttp) {
64+
if (meta.http) {
7465
transaction = oboe.Span.createHttpSpan({
7566
transaction,
7667
duration,
7768
has_error,
78-
method,
79-
status,
80-
url,
81-
domain: null,
69+
method: meta.method,
70+
status: meta.status,
71+
url: meta.url,
72+
domain: meta.hostname,
8273
})
8374
} else {
8475
transaction = oboe.Span.createSpan({
@@ -93,35 +84,3 @@ export class AppopticsInboundMetricsProcessor
9384
span.attributes[TRANSACTION_NAME_ATTRIBUTE] = transaction
9485
}
9586
}
96-
97-
export function httpSpanMetadata(span: ReadableSpan) {
98-
if (
99-
span.kind !== SpanKind.SERVER ||
100-
!(
101-
ATTR_HTTP_REQUEST_METHOD in span.attributes ||
102-
ATTR_HTTP_METHOD in span.attributes
103-
)
104-
) {
105-
return { isHttp: false } as const
106-
}
107-
108-
const method = String(
109-
span.attributes[ATTR_HTTP_REQUEST_METHOD] ??
110-
span.attributes[ATTR_HTTP_METHOD],
111-
)
112-
const status = Number(
113-
span.attributes[ATTR_HTTP_RESPONSE_STATUS_CODE] ??
114-
span.attributes[ATTR_HTTP_STATUS_CODE] ??
115-
0,
116-
)
117-
const url = String(
118-
span.attributes[ATTR_URL_FULL] ?? span.attributes[ATTR_HTTP_URL],
119-
)
120-
121-
return {
122-
isHttp: true,
123-
method,
124-
status,
125-
url,
126-
} as const
127-
}

0 commit comments

Comments
 (0)