@dynatrace/opentelemetry-core
Version:
Dynatrace OpenTelemetry core package
294 lines (291 loc) • 13.5 kB
JavaScript
"use strict";
/*
Copyright 2022 Dynatrace LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DtSpanExporter = void 0;
const http = require("http");
const https = require("https");
const path = require("path");
const perf_hooks_1 = require("perf_hooks");
const url = require("url");
const api_1 = require("@opentelemetry/api");
const core_1 = require("@opentelemetry/core");
const Types_1 = require("../Types");
const Logger_1 = require("../Logger");
const StartupConfig_1 = require("../configuration/StartupConfig");
const Resource_1 = require("../trace/Resource");
const LoggingUtils_1 = require("../utils/LoggingUtils");
const Timers_1 = require("../utils/Timers");
const Validators_1 = require("../utils/Validators");
const version_1 = require("../version");
const Serialization_1 = require("./Serialization");
// ============================================================================
const cSpansPath = "odin/v1/spans";
const cMaxRetries = 2;
const cDefaultFastConnectionTimeoutMs = 1000;
const cDefaultFastDataTimeoutMs = 5000;
const cDefaultNormalConnectionTimeoutMs = 10000;
const cDefaultNormalDataTimeoutMs = 60000;
const cConnectionResetTimeLimitMs = 300;
// ----------------------------------------------------------------------------
class DtSpanExporter {
constructor(cfg) {
this._connecting = false;
this._isShutdown = false;
this._hasPendingRequest = false;
this._logger = new Logger_1.ComponentLogger("Exporter");
this._suppressingContext = (0, core_1.suppressTracing)(api_1.ROOT_CONTEXT);
const config = Object.assign(Object.assign({}, (0, StartupConfig_1.getConfiguration)()), cfg);
if (config == null || typeof (config) !== "object") {
throw new TypeError("DtSpanExporter: config invalid");
}
if (typeof (config.baseUrl) !== "string") {
throw new TypeError("DtSpanExporter: baseUrl must be a string");
}
if (typeof (config.authToken) !== "string") {
throw new TypeError("DtSpanExporter: authToken must be a string");
}
if (typeof (config.tenantUUID) !== "string") {
throw new TypeError("DtSpanExporter: tenantUUID must be a string");
}
if (typeof (config.agentID) !== "object") {
throw new TypeError("DtSpanExporter: agentID must be a Long");
}
this._fastTimeoutsMs = {
connection: (0, Validators_1.checkNumericOption)("DtSpanExporter", config, "fastConnectionTimeoutMs", cDefaultFastConnectionTimeoutMs),
data: (0, Validators_1.checkNumericOption)("DtSpanExporter", config, "fastDataTimeoutMs", cDefaultFastDataTimeoutMs)
};
this._normalTimeoutsMs = {
connection: (0, Validators_1.checkNumericOption)("DtSpanExporter", config, "normalConnectionTimeoutMs", cDefaultNormalConnectionTimeoutMs),
data: (0, Validators_1.checkNumericOption)("DtSpanExporter", config, "normalDataTimeoutMs", cDefaultNormalDataTimeoutMs)
};
const urlObj = new url.URL(config.baseUrl);
let proto;
switch (urlObj.protocol) {
case "http:":
proto = http;
break;
case "https:":
proto = https;
break;
default: throw new RangeError("DtSpanExporter: options.baseUrl protocol unsupported");
}
this._httpRequest = proto.request;
this._tenantUUID = config.tenantUUID;
this._senderId = config.agentID;
this._reqOpts = {
method: "POST",
hostname: urlObj.hostname,
port: urlObj.port,
path: path.posix.join(urlObj.pathname, cSpansPath),
agent: new proto.Agent({
keepAlive: true,
maxSockets: 1
}),
headers: {
"Content-Type": "application/x-dt-span-export",
"Authorization": `dynatrace ${config.authToken}`,
"User-Agent": `odin-nodejs/${version_1.VERSIONS.fullVersion} 0x${Buffer.from(this._senderId.toBytes()).toString("hex")} ${config.tenantUUID}`
},
rejectUnauthorized: this._isSecurityEnhancedDomain(urlObj.hostname)
};
}
hasPendingRequest() {
return this._hasPendingRequest;
}
/**
* Exports spans.
*/
export(spans, resultCallback, type = Types_1.ExportType.Normal, retries = 0, spanExportMsg) {
if (spans.length === 0) {
process.nextTick(resultCallback, { code: core_1.ExportResultCode.SUCCESS });
return;
}
if (this._isShutdown || this._request) {
process.nextTick(resultCallback, { code: core_1.ExportResultCode.FAILED });
return;
}
api_1.context.with(this._suppressingContext, () => {
if (this._resource == null) {
// Save the resource on the first export
const resource = (0, Resource_1.detectResource)().merge(spans[0].resource);
this._resource = (0, Serialization_1.serializeResource)(resource);
this._logger.info(`Resource elements: ${Object.keys(resource.attributes).length}, size: ${this._resource.length}`);
if (this._logger.debugEnabled) {
this._logger.debug(`Resource values: ${(0, LoggingUtils_1.prettyPrintObject)(resource.attributes)}`);
}
}
// encode all Spans into SpanExport messages
spanExportMsg = spanExportMsg !== null && spanExportMsg !== void 0 ? spanExportMsg : (0, Serialization_1.serialize)(this._senderId, this._tenantUUID, spans, this._resource);
if (this._logger.debugEnabled) {
this._logger.debug(`export: spanCnt: ${spans.length}, size: ${spanExportMsg.length}, type: ${Types_1.ExportType[type]}, retries: ${retries}`);
}
this._executeRequest(spanExportMsg, spans, resultCallback, type, retries);
});
}
/**
* Shutdown exporter
*/
shutdown() {
this._logger.info("shutdown");
this._isShutdown = true;
return Promise.resolve();
}
/**
* Abort an ongoing export by destroying the underlying socket.
*/
abort() {
if (this._request) {
this._logger.debug("abort");
this._request.destroy();
}
}
_executeRequest(spanExportMsg, spans, resultCallback, type, retries) {
// set timeout depending of connection state of socket
const timeouts = (type === Types_1.ExportType.ForceFlush) ? this._fastTimeoutsMs : this._normalTimeoutsMs;
const self = this;
function onResponse(res) {
if (self._logger.debugEnabled) {
self._logger.debug(`request#onResponse: statusCode: ${res.statusCode}`);
}
self._stopTimer();
res.resume(); // discard any incoming data
res.on("error", (e) => {
// no need for handling a response error as a valid statusCode has
// been received before which indicates message was received
self._logger.debug(`response#error: ${(0, LoggingUtils_1.verboseFormatException)(e)}`);
});
if (res.statusCode != null && res.statusCode >= 200 && res.statusCode < 300) {
process.nextTick(resultCallback, { code: core_1.ExportResultCode.SUCCESS });
}
else if (res.statusCode === 401 || res.statusCode === 403) {
self._logger.warn("Not authorized to send spans");
// 401/403 is permanent
self._isShutdown = true;
process.nextTick(resultCallback, { code: core_1.ExportResultCode.FAILED });
}
else {
self._logger.warn(`Received status code ${res.statusCode}`);
process.nextTick(resultCallback, { code: core_1.ExportResultCode.FAILED });
}
self._request = undefined;
}
function onRequestError(e) {
const connecting = self._connecting;
self._connecting = false;
self._stopTimer();
self._request = undefined;
if (self._logger.debugEnabled) {
self._logger.debug(`request#error: connecting: ${connecting}\n${(0, LoggingUtils_1.verboseFormatException)(e)}`);
}
// when connection is reset by the server, retry only once with no delay if send operation didn't take longer than cConnectionResetTimeLimitMs
if (self._shouldRetrySendOperation(retries, e.code)) {
self._logger.debug("retrying export due to connection reset");
self.export(spans, resultCallback, type, retries + 1, spanExportMsg);
return;
}
if (!connecting) {
// error after connection before response was received. do not retry (possibly some chunks already sent)
process.nextTick(resultCallback, { code: core_1.ExportResultCode.FAILED });
return;
}
if (type === Types_1.ExportType.ForceFlush) {
// do not retry on errors during force flush
process.nextTick(resultCallback, { code: core_1.ExportResultCode.FAILED });
return;
}
if (retries < cMaxRetries) {
const retryMs = self._getDelayMs(retries);
if (self._logger.debugEnabled) {
self._logger.debug(`retry in ${retryMs}ms`);
}
(0, Timers_1.setTimeoutUnref)(() => self.export(spans, resultCallback, type, retries + 1, spanExportMsg), retryMs);
return;
}
process.nextTick(resultCallback, { code: core_1.ExportResultCode.FAILED });
}
function onSocket(socket) {
if (self._logger.debugEnabled) {
self._logger.debug(`request#socket: connecting: ${socket.connecting}`);
}
if (socket.connecting) {
socket.once("connect", onSocketConnect);
}
else {
onSocketConnect();
}
}
function onSocketConnect() {
self._logger.debug("socket#connect");
self._connecting = false;
self._stopTimer();
// monitor data transfer - end conditions:
// error event, response event
self._timer = (0, Timers_1.setTimeoutUnref)(() => {
self._logger.debug("timeout during data transfer");
self.abort();
}, timeouts.data);
self._sendStartTime = perf_hooks_1.performance.now();
}
try {
this._hasPendingRequest = true;
this._request = this._httpRequest(this._reqOpts, onResponse);
this._request.on("error", onRequestError);
this._request.on("socket", onSocket);
this._request.end(spanExportMsg);
// monitor connection setup - end conditions:
// error event, socket event with connected socket or connect event on socket
this._timer = (0, Timers_1.setTimeoutUnref)(() => {
this._logger.debug("timeout during connection setup");
this.abort();
}, timeouts.connection);
this._connecting = true;
}
catch (e) {
// this is clearly a setup issue as we provide invalid data => shutdown exporter
this._logger.logAgentException(e);
this._isShutdown = true;
process.nextTick(resultCallback, { code: core_1.ExportResultCode.FAILED });
}
finally {
this._hasPendingRequest = false;
}
}
_getDelayMs(retries) {
const [baseDelaySeconds, skewRangeSeconds] = (retries === 0) ? [5, 1] : [15, 5];
const skew = (Math.random() - 0.5) * skewRangeSeconds * 2;
return Math.round((baseDelaySeconds + skew) * 1000);
}
_isSecurityEnhancedDomain(domain) {
return (domain.indexOf(".dynatrace.com") !== -1) ||
(domain.indexOf(".dynatracelabs.com") !== -1) ||
(domain.indexOf(".ruxit.com") !== -1) ||
(domain.indexOf(".ruxitlabs.com") !== -1);
}
_stopTimer() {
if (this._timer != null) {
clearTimeout(this._timer);
}
this._timer = undefined;
}
_shouldRetrySendOperation(retries, errorCode) {
let shouldRetry = false;
const elapsedTimeMs = perf_hooks_1.performance.now() - this._sendStartTime;
if (retries === 0 && elapsedTimeMs < cConnectionResetTimeLimitMs && errorCode === "ECONNRESET") {
shouldRetry = true;
}
return shouldRetry;
}
}
exports.DtSpanExporter = DtSpanExporter;
//# sourceMappingURL=DtSpanExporter.js.map