@azure/monitor-opentelemetry
Version:
Azure Monitor OpenTelemetry (Node.js)
541 lines • 22.1 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { KnownDocumentType } from "../../generated/index.js";
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
import { SEMATTRS_EXCEPTION_MESSAGE, SEMATTRS_NET_PEER_PORT, SEMATTRS_RPC_GRPC_STATUS_CODE, SEMRESATTRS_TELEMETRY_SDK_VERSION, SEMATTRS_EXCEPTION_STACKTRACE, SEMATTRS_DB_SYSTEM, SEMATTRS_RPC_SYSTEM, DBSYSTEMVALUES_MONGODB, DBSYSTEMVALUES_MYSQL, DBSYSTEMVALUES_POSTGRESQL, DBSYSTEMVALUES_REDIS, SEMATTRS_DB_NAME, SEMATTRS_DB_OPERATION, SEMATTRS_DB_STATEMENT, ATTR_CLIENT_ADDRESS, SEMATTRS_NET_PEER_NAME, SEMATTRS_HTTP_HOST, ATTR_SERVER_ADDRESS, SEMATTRS_HTTP_TARGET, ATTR_URL_QUERY, ATTR_URL_PATH, SEMATTRS_HTTP_SCHEME, ATTR_URL_SCHEME, ATTR_HTTP_RESPONSE_STATUS_CODE, SEMATTRS_HTTP_STATUS_CODE, ATTR_HTTP_REQUEST_METHOD, SEMATTRS_HTTP_METHOD, ATTR_URL_FULL, SEMATTRS_HTTP_URL, ATTR_NETWORK_PEER_ADDRESS, SEMATTRS_NET_PEER_IP, ATTR_USER_AGENT_ORIGINAL, SEMATTRS_HTTP_USER_AGENT, ATTR_SERVER_PORT, ATTR_CLIENT_PORT, SEMATTRS_NET_HOST_PORT, } from "@opentelemetry/semantic-conventions";
import { SDK_INFO, hrTimeToMilliseconds } from "@opentelemetry/core";
import { DataPointType } from "@opentelemetry/sdk-metrics";
import { APPLICATION_INSIGHTS_SHIM_VERSION, AZURE_MONITOR_AUTO_ATTACH, AZURE_MONITOR_OPENTELEMETRY_VERSION, AZURE_MONITOR_PREFIX, AttachTypePrefix, } from "../../types.js";
import { QuickPulseMetricNames, QuickPulseOpenTelemetryMetricNames, DependencyTypes, legacySemanticValues, httpSemanticValues, } from "./types.js";
import { getOsPrefix } from "../../utils/common.js";
import { getResourceProvider } from "../../utils/common.js";
import { getDependencyTarget, isSqlDB, isExceptionTelemetry } from "../utils.js";
import { Logger } from "../../shared/logging/index.js";
/** Get the internal SDK version */
export function getSdkVersion() {
const { nodeVersion } = process.versions;
const opentelemetryVersion = SDK_INFO[SEMRESATTRS_TELEMETRY_SDK_VERSION];
const version = getSdkVersionType();
const internalSdkVersion = `${process.env[AZURE_MONITOR_PREFIX] ?? ""}node${nodeVersion}:otel${opentelemetryVersion}:${version}`;
return internalSdkVersion;
}
/** Get the internal SDK version type */
export function getSdkVersionType() {
if (process.env[APPLICATION_INSIGHTS_SHIM_VERSION]) {
return `sha${process.env[APPLICATION_INSIGHTS_SHIM_VERSION]}`;
}
else {
return `dst${AZURE_MONITOR_OPENTELEMETRY_VERSION}`;
}
}
// eslint-disable-next-line tsdoc/syntax
/** Set the version prefix to a string in the format "{ResourceProvider}{OS}m_ */
export function setSdkPrefix() {
if (!process.env[AZURE_MONITOR_PREFIX]) {
const prefixAttachType = process.env[AZURE_MONITOR_AUTO_ATTACH] === "true"
? AttachTypePrefix.INTEGRATED_AUTO
: AttachTypePrefix.MANUAL;
process.env[AZURE_MONITOR_PREFIX] =
`${getResourceProvider()}${getOsPrefix()}${prefixAttachType}_`;
}
}
export function resourceMetricsToQuickpulseDataPoint(metrics, baseMonitoringDataPoint, documents, errors, derivedMetricValues) {
const metricPoints = [];
metrics.scopeMetrics.forEach((scopeMetric) => {
scopeMetric.metrics.forEach((metric) => {
metric.dataPoints.forEach((dataPoint) => {
const metricPoint = {
weight: 1,
name: "",
value: 0,
};
// Update name to expected value in Quickpulse, needed because those names are invalid in OTel
switch (metric.descriptor.name) {
case QuickPulseOpenTelemetryMetricNames.PHYSICAL_BYTES:
metricPoint.name = QuickPulseMetricNames.PHYSICAL_BYTES;
break;
case QuickPulseOpenTelemetryMetricNames.DEPENDENCY_DURATION:
metricPoint.name = QuickPulseMetricNames.DEPENDENCY_DURATION;
break;
case QuickPulseOpenTelemetryMetricNames.DEPENDENCY_FAILURE_RATE:
metricPoint.name = QuickPulseMetricNames.DEPENDENCY_FAILURE_RATE;
break;
case QuickPulseOpenTelemetryMetricNames.DEPENDENCY_RATE:
metricPoint.name = QuickPulseMetricNames.DEPENDENCY_RATE;
break;
case QuickPulseOpenTelemetryMetricNames.EXCEPTION_RATE:
metricPoint.name = QuickPulseMetricNames.EXCEPTION_RATE;
break;
case QuickPulseOpenTelemetryMetricNames.PROCESSOR_TIME_NORMALIZED:
metricPoint.name = QuickPulseMetricNames.PROCESSOR_TIME_NORMALIZED;
break;
case QuickPulseOpenTelemetryMetricNames.REQUEST_DURATION:
metricPoint.name = QuickPulseMetricNames.REQUEST_DURATION;
break;
case QuickPulseOpenTelemetryMetricNames.REQUEST_FAILURE_RATE:
metricPoint.name = QuickPulseMetricNames.REQUEST_FAILURE_RATE;
break;
case QuickPulseOpenTelemetryMetricNames.REQUEST_RATE:
metricPoint.name = QuickPulseMetricNames.REQUEST_RATE;
break;
default:
metricPoint.name = metric.descriptor.name;
}
if (metric.dataPointType === DataPointType.SUM ||
metric.dataPointType === DataPointType.GAUGE) {
metricPoint.value = dataPoint.value;
}
else {
metricPoint.value = dataPoint.value.sum || 0;
}
metricPoints.push(metricPoint);
// TODO: remove the metric points with the old metric names after
// UI side has done their changes to support the new names.
if (metricPoint.name === QuickPulseMetricNames.PHYSICAL_BYTES ||
metricPoint.name === QuickPulseMetricNames.PROCESSOR_TIME_NORMALIZED) {
const oldMetricPoint = {
weight: 1,
name: metricPoint.name === QuickPulseMetricNames.PHYSICAL_BYTES
? QuickPulseMetricNames.COMMITTED_BYTES
: QuickPulseMetricNames.PROCESSOR_TIME,
value: dataPoint.value,
};
metricPoints.push(oldMetricPoint);
}
});
});
});
derivedMetricValues.forEach((value, id) => {
const metricPoint = {
weight: 1,
name: id,
value: value,
};
metricPoints.push(metricPoint);
});
const quickpulseDataPoint = {
...baseMonitoringDataPoint,
timestamp: new Date(),
metrics: metricPoints,
documents: documents,
collectionConfigurationErrors: errors,
};
return [quickpulseDataPoint];
}
function getIso8601Duration(milliseconds) {
const seconds = milliseconds / 1000;
return `PT${seconds}S`;
}
export function getSpanData(span) {
if (span.kind === SpanKind.SERVER || span.kind === SpanKind.CONSUMER) {
// request
return getRequestData(span);
}
else {
// dependency
return getDependencyData(span);
}
}
export function getSpanExceptionColumns(eventAttributes, spanAttributes) {
const exceptionData = {
Message: eventAttributes[SEMATTRS_EXCEPTION_MESSAGE],
StackTrace: eventAttributes[SEMATTRS_EXCEPTION_STACKTRACE],
CustomDimensions: createCustomDimsFromAttributes(spanAttributes),
};
return exceptionData;
}
// A slightly modified version of createRequestData from spanUtils in exporter
function getRequestData(span) {
const requestData = {
Url: "",
Duration: hrTimeToMilliseconds(span.duration),
ResponseCode: 0,
Success: false,
Name: span.name || "",
CustomDimensions: createCustomDimsFromAttributes(span.attributes),
};
const httpMethod = getHttpMethod(span.attributes);
const grpcStatusCode = span.attributes[SEMATTRS_RPC_GRPC_STATUS_CODE];
if (httpMethod) {
requestData.Url = getUrl(span.attributes);
if (URL.canParse(requestData.Url)) {
const urlObj = new URL(requestData.Url);
requestData.Name = `${httpMethod} ${urlObj.pathname}`;
}
else {
Logger.getInstance().info("Request data sent to live metrics has no valid URL field.");
}
const httpStatusCode = getHttpStatusCode(span.attributes);
if (httpStatusCode) {
requestData.ResponseCode = Number(httpStatusCode);
}
}
else if (grpcStatusCode) {
requestData.ResponseCode = Number(grpcStatusCode);
}
requestData.Success = span.status.code !== SpanStatusCode.ERROR && requestData.ResponseCode < 400;
return requestData;
}
// A slightly modified version of createDependencyData from spanUtils in exporter
function getDependencyData(span) {
const dependencyData = {
Target: "",
Duration: hrTimeToMilliseconds(span.duration),
Success: span.status.code !== SpanStatusCode.ERROR,
Name: span.name,
ResultCode: 0,
Type: "",
Data: "",
CustomDimensions: createCustomDimsFromAttributes(span.attributes),
};
if (span.kind === SpanKind.PRODUCER) {
dependencyData.Type = DependencyTypes.QueueMessage;
}
if (span.kind === SpanKind.INTERNAL && span.parentSpanContext?.spanId) {
dependencyData.Type = DependencyTypes.InProc;
}
const httpMethod = getHttpMethod(span.attributes);
const dbSystem = span.attributes[SEMATTRS_DB_SYSTEM];
const rpcSystem = span.attributes[SEMATTRS_RPC_SYSTEM];
// HTTP Dependency
if (httpMethod) {
const httpUrl = getHttpUrl(span.attributes);
if (httpUrl) {
if (URL.canParse(String(httpUrl))) {
const dependencyUrl = new URL(String(httpUrl));
dependencyData.Name = `${httpMethod} ${dependencyUrl.pathname}`;
}
else {
Logger.getInstance().info("Dependency data sent to live metrics has no valid URL field.");
}
}
dependencyData.Type = DependencyTypes.Http;
dependencyData.Data = getUrl(span.attributes);
const httpStatusCode = getHttpStatusCode(span.attributes);
if (httpStatusCode) {
dependencyData.ResultCode = Number(httpStatusCode);
}
let target = getDependencyTarget(span.attributes);
if (target) {
try {
// Remove default port
const portRegex = new RegExp(/(https?)(:\/\/.*)(:\d+)(\S*)/);
const res = portRegex.exec(target);
if (res !== null) {
const protocol = res[1];
const port = res[3];
if ((protocol === "https" && port === ":443") ||
(protocol === "http" && port === ":80")) {
// Drop port
target = res[1] + res[2] + res[4];
}
}
}
catch (ex) {
/* no-op */
}
dependencyData.Target = `${target}`;
}
}
// DB Dependency
else if (dbSystem) {
// TODO: Remove special logic when Azure UX supports OpenTelemetry dbSystem
if (String(dbSystem) === DBSYSTEMVALUES_MYSQL) {
dependencyData.Type = DependencyTypes.mysql;
}
else if (String(dbSystem) === DBSYSTEMVALUES_POSTGRESQL) {
dependencyData.Type = DependencyTypes.postgresql;
}
else if (String(dbSystem) === DBSYSTEMVALUES_MONGODB) {
dependencyData.Type = DependencyTypes.mongodb;
}
else if (String(dbSystem) === DBSYSTEMVALUES_REDIS) {
dependencyData.Type = DependencyTypes.redis;
}
else if (isSqlDB(String(dbSystem))) {
dependencyData.Type = DependencyTypes.Sql;
}
else {
dependencyData.Type = String(dbSystem);
}
const dbStatement = span.attributes[SEMATTRS_DB_STATEMENT];
const dbOperation = span.attributes[SEMATTRS_DB_OPERATION];
if (dbStatement) {
dependencyData.Data = String(dbStatement);
}
else if (dbOperation) {
dependencyData.Data = String(dbOperation);
}
const target = getDependencyTarget(span.attributes);
const dbName = span.attributes[SEMATTRS_DB_NAME];
if (target) {
dependencyData.Target = dbName ? `${target}|${dbName}` : `${target}`;
}
else {
dependencyData.Target = dbName ? `${dbName}` : `${dbSystem}`;
}
}
// grpc Dependency
else if (rpcSystem) {
if (rpcSystem === DependencyTypes.Wcf) {
dependencyData.Type = DependencyTypes.Wcf;
}
else {
dependencyData.Type = DependencyTypes.Grpc;
}
const grpcStatusCode = span.attributes[SEMATTRS_RPC_GRPC_STATUS_CODE];
if (grpcStatusCode) {
dependencyData.ResultCode = Number(grpcStatusCode);
}
const target = getDependencyTarget(span.attributes);
if (target) {
dependencyData.Target = `${target}`;
}
else if (rpcSystem) {
dependencyData.Target = String(rpcSystem);
}
}
return dependencyData;
}
export function getLogData(log) {
const customDims = createCustomDimsFromAttributes(log.attributes);
if (isExceptionTelemetry(log)) {
return {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
Message: String(log.attributes[SEMATTRS_EXCEPTION_MESSAGE]),
// eslint-disable-next-line @typescript-eslint/no-base-to-string
StackTrace: String(log.attributes[SEMATTRS_EXCEPTION_STACKTRACE]),
CustomDimensions: customDims,
};
}
else {
return {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
Message: String(log.body),
CustomDimensions: customDims,
};
}
}
export function getLogDocument(data, exceptionType) {
if (isExceptionData(data) && exceptionType) {
return {
documentType: KnownDocumentType.Exception,
exceptionMessage: data.Message,
exceptionType: exceptionType,
properties: mapToKeyValuePairList(data.CustomDimensions),
};
}
else {
// trace
return {
documentType: KnownDocumentType.Trace,
message: data.Message,
properties: mapToKeyValuePairList(data.CustomDimensions),
};
}
}
export function isRequestData(data) {
return data.Url !== undefined;
}
export function isDependencyData(data) {
return data.Target !== undefined;
}
export function isTraceData(data) {
return data.Message !== undefined && data.StackTrace === undefined;
}
export function isExceptionData(data) {
return data.StackTrace !== undefined;
}
export function getSpanDocument(telemetryData) {
let document = {
documentType: KnownDocumentType.Request,
};
if (isRequestData(telemetryData)) {
document = {
documentType: KnownDocumentType.Request,
name: telemetryData.Name,
url: telemetryData.Url,
responseCode: String(telemetryData.ResponseCode),
duration: getIso8601Duration(telemetryData.Duration),
};
}
else if (isDependencyData(telemetryData)) {
document = {
documentType: KnownDocumentType.RemoteDependency,
name: telemetryData.Name,
commandName: telemetryData.Data,
resultCode: String(telemetryData.ResultCode),
duration: getIso8601Duration(telemetryData.Duration),
};
}
document.properties = mapToKeyValuePairList(telemetryData.CustomDimensions);
return document;
}
function createCustomDimsFromAttributes(attributes) {
const customDims = new Map();
if (attributes) {
for (const key of Object.keys(attributes)) {
if (!(key.startsWith("_MS.") ||
legacySemanticValues.includes(key) ||
httpSemanticValues.includes(key))) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
customDims.set(key, String(attributes[key]));
}
}
}
return customDims;
}
function mapToKeyValuePairList(map) {
const list = [];
map.forEach((value, key) => {
list.push({ key, value });
});
return list;
}
function getUrl(attributes) {
if (!attributes) {
return "";
}
const httpMethod = getHttpMethod(attributes);
if (httpMethod) {
const httpUrl = getHttpUrl(attributes);
if (httpUrl) {
return String(httpUrl);
}
else {
const httpScheme = getHttpScheme(attributes);
const httpTarget = getHttpTarget(attributes);
if (httpScheme && httpTarget) {
const httpHost = getHttpHost(attributes);
if (httpHost) {
return `${httpScheme}://${httpHost}${httpTarget}`;
}
else {
const netPeerPort = getNetPeerPort(attributes);
if (netPeerPort) {
const netPeerName = getNetPeerName(attributes);
if (netPeerName) {
return `${httpScheme}://${netPeerName}:${netPeerPort}${httpTarget}`;
}
else {
const netPeerIp = getPeerIp(attributes);
if (netPeerIp) {
return `${httpScheme}://${netPeerIp}:${netPeerPort}${httpTarget}`;
}
}
}
}
}
}
}
return "";
}
/**
* UTC time the request was made. Expressed as the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight on January 1, 0001. This is used for clock skew calculations, so the value can never be stale (cached).
*
* @example
* 8/5/2020 10:15:00 PM UTC =\> 637322625000000000
* 8/5/2020 10:15:01 PM UTC =\> 637322625010000000
*/
export function getTransmissionTime() {
return (Date.now() + 62135596800000) * 10000;
}
export function getMsFromFilterTimestampString(timestamp) {
// The service side will return a timestamp in the following format:
// [days].[hours]:[minutes]:[seconds]
// the seconds may be a whole number or something like 7.89. 7.89 seconds translates to 7890 ms.
// writing this method because date.getmilliseconds() returns incorrect result on large timestamps.
// examples: "14.6:56:7.89" = 1234567890 ms, "0.0:0:0.2" = 200 ms
const parts = timestamp.split(":");
if (parts.length !== 3) {
return NaN;
}
const seconds = parseFloat(parts[2]);
const minutes = parseFloat(parts[1]);
const firstPart = parts[0].split(".");
if (firstPart.length !== 2) {
return NaN;
}
const hours = parseFloat(firstPart[1]);
const days = parseFloat(firstPart[0]);
if (isNaN(days) || isNaN(hours) || isNaN(minutes) || isNaN(seconds)) {
return NaN;
}
return seconds * 1000 + minutes * 60000 + hours * 3600000 + days * 86400000;
}
export function getPeerIp(attributes) {
if (attributes) {
return String(attributes[ATTR_NETWORK_PEER_ADDRESS] || attributes[SEMATTRS_NET_PEER_IP]);
}
return;
}
export function getUserAgent(attributes) {
if (attributes) {
return String(attributes[ATTR_USER_AGENT_ORIGINAL] || attributes[SEMATTRS_HTTP_USER_AGENT]);
}
return;
}
export function getHttpUrl(attributes) {
// Stable sem conv only supports populating url from `url.full`
if (attributes) {
return String(attributes[ATTR_URL_FULL] || attributes[SEMATTRS_HTTP_URL] || "");
}
return "";
}
export function getHttpMethod(attributes) {
if (attributes) {
return String(attributes[ATTR_HTTP_REQUEST_METHOD] || attributes[SEMATTRS_HTTP_METHOD]);
}
return;
}
export function getHttpStatusCode(attributes) {
if (attributes) {
return String(attributes[ATTR_HTTP_RESPONSE_STATUS_CODE] || attributes[SEMATTRS_HTTP_STATUS_CODE]);
}
return;
}
export function getHttpScheme(attributes) {
if (attributes) {
return String(attributes[ATTR_URL_SCHEME] || attributes[SEMATTRS_HTTP_SCHEME]);
}
return;
}
export function getHttpTarget(attributes) {
if (attributes) {
if (attributes[ATTR_URL_PATH]) {
return String(attributes[ATTR_URL_PATH]);
}
if (attributes[ATTR_URL_QUERY]) {
return String(attributes[ATTR_URL_QUERY]);
}
return String(attributes[SEMATTRS_HTTP_TARGET] || "");
}
return "";
}
export function getHttpHost(attributes) {
if (attributes) {
return String(attributes[ATTR_SERVER_ADDRESS] || attributes[SEMATTRS_HTTP_HOST]);
}
return;
}
export function getNetPeerName(attributes) {
if (attributes) {
return String(attributes[ATTR_CLIENT_ADDRESS] || attributes[SEMATTRS_NET_PEER_NAME] || "");
}
return "";
}
export function getNetHostPort(attributes) {
if (attributes) {
return String(attributes[ATTR_SERVER_PORT] || attributes[SEMATTRS_NET_HOST_PORT] || "");
}
return "";
}
export function getNetPeerPort(attributes) {
if (attributes) {
return String(attributes[ATTR_CLIENT_PORT] ||
attributes[ATTR_SERVER_PORT] ||
attributes[SEMATTRS_NET_PEER_PORT]);
}
return;
}
//# sourceMappingURL=utils.js.map