Skip to content

Commit e8a13e5

Browse files
authored
Create OTLP Metrics Exporter in AKS Auto-Attach Scenarios (#1467)
* Create OTLP metric exporter in AKS agent init and fix tests. * Update deps. * Update dependencies. * Update OTel deps. * Address comments. * Update aksLoader.tests.ts
1 parent eeadb2d commit e8a13e5

File tree

12 files changed

+719
-1755
lines changed

12 files changed

+719
-1755
lines changed

package-lock.json

Lines changed: 534 additions & 1726 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,22 @@
6868
"@azure/functions": "^4.6.0",
6969
"@azure/functions-old": "npm:@azure/[email protected]",
7070
"@azure/identity": "^4.6.0",
71-
"@azure/monitor-opentelemetry": "^1.12.0",
72-
"@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.33",
71+
"@azure/monitor-opentelemetry": "^1.13.1",
72+
"@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.34",
7373
"@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.7",
7474
"@opentelemetry/api": "^1.9.0",
75-
"@opentelemetry/api-logs": "^0.202.0",
76-
"@opentelemetry/core": "^2.0.1",
77-
"@opentelemetry/exporter-logs-otlp-http": "^0.202.0",
78-
"@opentelemetry/exporter-metrics-otlp-http": "^0.202.0",
79-
"@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
80-
"@opentelemetry/otlp-exporter-base": "^0.202.0",
81-
"@opentelemetry/resources": "^2.0.1",
82-
"@opentelemetry/sdk-logs": "0.200.0",
83-
"@opentelemetry/sdk-metrics": "^2.0.1",
84-
"@opentelemetry/sdk-trace-base": "^2.0.1",
85-
"@opentelemetry/sdk-trace-node": "^2.0.1",
86-
"@opentelemetry/semantic-conventions": "^1.34.0",
75+
"@opentelemetry/api-logs": "^0.204.0",
76+
"@opentelemetry/core": "^2.1.0",
77+
"@opentelemetry/exporter-logs-otlp-http": "^0.204.0",
78+
"@opentelemetry/exporter-metrics-otlp-http": "^0.204.0",
79+
"@opentelemetry/exporter-trace-otlp-http": "^0.204.0",
80+
"@opentelemetry/otlp-exporter-base": "^0.204.0",
81+
"@opentelemetry/resources": "^2.1.0",
82+
"@opentelemetry/sdk-logs": "^0.204.0",
83+
"@opentelemetry/sdk-metrics": "^2.1.0",
84+
"@opentelemetry/sdk-trace-base": "^2.1.0",
85+
"@opentelemetry/sdk-trace-node": "^2.1.0",
86+
"@opentelemetry/semantic-conventions": "^1.32.0",
8787
"diagnostic-channel": "1.1.1",
8888
"diagnostic-channel-publishers": "1.0.8"
8989
}

src/agent/aksLoader.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { FileWriter } from "./diagnostics/writers/fileWriter";
88
import { StatusLogger } from "./diagnostics/statusLogger";
99
import { AgentLoader } from "./agentLoader";
1010
import { InstrumentationOptions } from '../types';
11+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
12+
import { MetricReader, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
13+
import { OTLP_METRIC_EXPORTER_EXPORT_INTERVAL } from './types';
1114

1215
export class AKSLoader extends AgentLoader {
1316

@@ -50,6 +53,35 @@ export class AKSLoader extends AgentLoader {
5053
}
5154
)
5255
);
56+
57+
// Create metricReaders array and add OTLP reader if environment variables request it
58+
try {
59+
const metricReaders: MetricReader[] = [];
60+
if (
61+
process.env.OTEL_METRICS_EXPORTER === "otlp" &&
62+
(process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT)
63+
) {
64+
try {
65+
const otlpExporter = new OTLPMetricExporter();
66+
67+
const otlpMetricReader = new PeriodicExportingMetricReader({
68+
exporter: otlpExporter,
69+
exportIntervalMillis: OTLP_METRIC_EXPORTER_EXPORT_INTERVAL,
70+
});
71+
72+
metricReaders.push(otlpMetricReader);
73+
} catch (error) {
74+
console.warn("AKSLoader: Failed to create OTLP metric reader:", error);
75+
}
76+
}
77+
78+
// Attach metricReaders to the options so the distro can consume them
79+
if ((metricReaders || []).length > 0) {
80+
this._options.metricReaders = metricReaders;
81+
}
82+
} catch (err) {
83+
console.warn("AKSLoader: Error while preparing metricReaders:", err);
84+
}
5385
}
5486
}
5587
}

src/agent/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export const NODE_JS_RUNTIME_MAJOR_VERSION = parseInt(process.versions.node.spli
44
export const AZURE_APP_NAME = process.env.WEBSITE_SITE_NAME || 'unknown';
55
export const AZURE_MONITOR_AUTO_ATTACH = "AZURE_MONITOR_AUTO_ATTACH";
66

7+
export const OTLP_METRIC_EXPORTER_EXPORT_INTERVAL = 60000; // in ms
8+
79
export interface IAgentLogger {
810
log(message: any, ...optional: any[]): void;
911
}

src/shared/util/attributeLogRecordProcessor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LogRecord, LogRecordProcessor } from "@opentelemetry/sdk-logs";
1+
import { LogRecordProcessor, SdkLogRecord } from "@opentelemetry/sdk-logs";
22

33
export class AttributeLogProcessor implements LogRecordProcessor {
44
private _attributes: { [key: string]: string };
@@ -7,7 +7,7 @@ export class AttributeLogProcessor implements LogRecordProcessor {
77
}
88

99
// Override onEmit to apply log record attributes before exporting
10-
onEmit(record: LogRecord) {
10+
onEmit(record: SdkLogRecord) {
1111
record.setAttributes(this._attributes);
1212
}
1313

src/shim/logsApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { Logger as OtelLogger, LogRecord } from "@opentelemetry/api-logs";
5-
import { LogRecord as SDKLogRecord } from "@opentelemetry/sdk-logs";
5+
import { SdkLogRecord as SDKLogRecord } from "@opentelemetry/sdk-logs";
66
import { Attributes, diag } from "@opentelemetry/api";
77
import { IdGenerator, RandomIdGenerator } from "@opentelemetry/sdk-trace-base";
88

test/unitTests/agent/aksLoader.tests.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,127 @@ describe("agent/AKSLoader", () => {
6060
let loggerProvider = logs.getLoggerProvider() as any;
6161
assert.equal(loggerProvider.constructor.name, "LoggerProvider");
6262
});
63+
64+
it("constructor creates OTLP metric reader when environment variables are set", () => {
65+
const env = {
66+
["APPLICATIONINSIGHTS_CONNECTION_STRING"]: "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333",
67+
["OTEL_METRICS_EXPORTER"]: "otlp",
68+
["OTEL_EXPORTER_OTLP_ENDPOINT"]: "http://localhost:4317"
69+
};
70+
process.env = env;
71+
72+
const agent = new AKSLoader();
73+
74+
// Verify that metricReaders were added to the options
75+
const options = (agent as any)._options;
76+
assert.ok(options.metricReaders, "metricReaders should be present in options");
77+
assert.equal(options.metricReaders.length, 1, "Should have exactly one metric reader");
78+
79+
// Verify the metric reader is a PeriodicExportingMetricReader
80+
const metricReader = options.metricReaders[0];
81+
assert.equal(metricReader.constructor.name, "PeriodicExportingMetricReader", "Should be a PeriodicExportingMetricReader");
82+
83+
// Verify the exporter is an OTLP exporter
84+
const exporter = (metricReader as any)._exporter;
85+
assert.equal(exporter.constructor.name, "OTLPMetricExporter", "Should be an OTLPMetricExporter");
86+
87+
// Check that the URL is configured in parameters
88+
const delegate = (exporter as any)._delegate;
89+
const transport = delegate._transport;
90+
const innerTransport = transport._transport;
91+
const parameters = innerTransport._parameters;
92+
93+
const url = parameters.url || parameters.endpoint;
94+
assert.ok(url, "Parameters should have a URL configured");
95+
assert.equal(url.replace(/\/$/, ""), "http://localhost:4317/v1/metrics", "Should use the base OTLP endpoint URL");
96+
97+
// Verify the exporter type
98+
assert.ok(exporter, "Exporter should exist");
99+
assert.equal(exporter.constructor.name, "OTLPMetricExporter", "Should be an OTLPMetricExporter");
100+
});
101+
102+
it("constructor creates OTLP metric reader with metrics-specific endpoint", () => {
103+
const env = {
104+
["APPLICATIONINSIGHTS_CONNECTION_STRING"]: "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333",
105+
["OTEL_METRICS_EXPORTER"]: "otlp",
106+
["OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"]: "http://localhost:4318/v1/metrics"
107+
};
108+
process.env = env;
109+
110+
const agent = new AKSLoader();
111+
112+
// Verify that metricReaders were added to the options
113+
const options = (agent as any)._options;
114+
assert.ok(options.metricReaders, "metricReaders should be present in options");
115+
assert.equal(options.metricReaders.length, 1, "Should have exactly one metric reader");
116+
117+
// Verify the exporter URL uses the metrics-specific endpoint
118+
const metricReader = options.metricReaders[0];
119+
const exporter = (metricReader as any)._exporter;
120+
121+
// Check the configured URL in the transport parameters
122+
const delegate = (exporter as any)._delegate;
123+
const transport = delegate._transport;
124+
const innerTransport = transport._transport;
125+
const parameters = innerTransport._parameters;
126+
127+
const url = parameters.url || parameters.endpoint;
128+
assert.ok(url, "Exporter should have a URL configured");
129+
assert.equal(url, "http://localhost:4318/v1/metrics", "Should use the metrics-specific OTLP endpoint URL");
130+
});
131+
132+
it("constructor does not create OTLP metric reader when OTEL_METRICS_EXPORTER is not otlp", () => {
133+
const env = {
134+
["APPLICATIONINSIGHTS_CONNECTION_STRING"]: "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333",
135+
["OTEL_METRICS_EXPORTER"]: "console",
136+
["OTEL_EXPORTER_OTLP_ENDPOINT"]: "http://localhost:4317"
137+
};
138+
process.env = env;
139+
140+
const agent = new AKSLoader();
141+
142+
// Verify that no metricReaders were added to the options
143+
const options = (agent as any)._options;
144+
assert.ok(!options.metricReaders || options.metricReaders.length === 0, "Should not have any metric readers when OTEL_METRICS_EXPORTER is not 'otlp'");
145+
});
146+
147+
it("constructor does not create OTLP metric reader when no endpoint is provided", () => {
148+
const env = {
149+
["APPLICATIONINSIGHTS_CONNECTION_STRING"]: "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333",
150+
["OTEL_METRICS_EXPORTER"]: "otlp"
151+
// No OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
152+
};
153+
process.env = env;
154+
155+
const agent = new AKSLoader();
156+
157+
// Verify that no metricReaders were added to the options
158+
const options = (agent as any)._options;
159+
assert.ok(!options.metricReaders || options.metricReaders.length === 0, "Should not have any metric readers when no OTLP endpoint is provided");
160+
});
161+
162+
it("initialize with OTLP metric reader creates multiple metric collectors", () => {
163+
const env = {
164+
["APPLICATIONINSIGHTS_CONNECTION_STRING"]: "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333",
165+
["OTEL_METRICS_EXPORTER"]: "otlp",
166+
["OTEL_EXPORTER_OTLP_ENDPOINT"]: "http://localhost:4317"
167+
};
168+
process.env = env;
169+
170+
const agent = new AKSLoader();
171+
agent.initialize();
172+
173+
let meterProvider = metrics.getMeterProvider() as any;
174+
assert.equal(meterProvider.constructor.name, "MeterProvider");
175+
176+
// Should have both Azure Monitor and OTLP metric readers
177+
const metricCollectors = meterProvider["_sharedState"]["metricCollectors"];
178+
assert.ok(metricCollectors.length >= 1, "Should have at least one metric collector (Azure Monitor)");
179+
180+
// Check that we have at least one Azure Monitor exporter
181+
const azureMonitorExporters = metricCollectors.filter((collector: any) =>
182+
collector["_metricReader"]["_exporter"].constructor.name === "AzureMonitorMetricExporter"
183+
);
184+
assert.equal(azureMonitorExporters.length, 1, "Should have exactly one Azure Monitor metric exporter");
185+
});
63186
});
File renamed without changes.
File renamed without changes.

test/unitTests/logs/api.tests.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import assert from "assert";
44
import sinon from "sinon";
55
import nock from "nock";
66
import { Logger } from "@opentelemetry/api-logs";
7-
import { LogRecord } from "@opentelemetry/sdk-logs";
7+
import { SdkLogRecord } from "@opentelemetry/sdk-logs";
88

99
import {
1010
AvailabilityTelemetry,
@@ -40,9 +40,9 @@ describe("logs/API", () => {
4040

4141
class TestLogger implements Logger {
4242

43-
public logsEmited: Array<LogRecord> = [];
43+
public logsEmited: Array<SdkLogRecord> = [];
4444

45-
emit(logRecord: LogRecord): void {
45+
emit(logRecord: SdkLogRecord): void {
4646
this.logsEmited.push(logRecord);
4747
}
4848
}
@@ -59,7 +59,7 @@ describe("logs/API", () => {
5959
telemetry,
6060
"TestData",
6161
data,
62-
) as LogRecord;
62+
) as SdkLogRecord;
6363
assert.equal(JSON.stringify(logRecord.body), JSON.stringify({}));
6464
assert.equal(logRecord.attributes["testAttribute"], "testValue");
6565
assert.equal(logRecord.attributes["_MS.baseType"], "TestData");
@@ -77,7 +77,7 @@ describe("logs/API", () => {
7777
telemetry,
7878
"TestData",
7979
data,
80-
) as LogRecord;
80+
) as SdkLogRecord;
8181
assert.equal(JSON.stringify(logRecord.body), JSON.stringify({}));
8282
assert.equal(logRecord.attributes["testAttribute"], "testValue");
8383
const errorStr: string = logRecord.attributes["error"] as string;

0 commit comments

Comments
 (0)