Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.EventHubsExporterIntegrationTest.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.LiveMetrics.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.ReadmeSamples.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.SimpleWebAppSample.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.AiSemanticAttributes.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.AttributeKeyTemplate.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.AzureMonitorLogRecordExporterProvider.java" checks="IllegalImportCheck" />
Expand Down Expand Up @@ -130,6 +131,7 @@
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.semconv.incubating.UrlIncubatingAttributes.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.FeatureStatsbeat.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.NetworkStatsbeat.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.CustomerSdkStatsTelemetryPipelineListener.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.StatsbeatTelemetryPipelineListener.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.AksResourceAttributes.java" checks="IllegalImportCheck" />
<suppress files="com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.AksResourceAttributesTest.java" checks="IllegalImportCheck" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@
import com.azure.monitor.opentelemetry.autoconfigure.implementation.configuration.StatsbeatConnectionString;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.heartbeat.HeartbeatExporter;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.ContextTagKeys;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.TelemetryItem;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.pipeline.TelemetryItemExporter;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.QuickPulse;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.CustomerSdkStats;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.CustomerSdkStatsTelemetryPipelineListener;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.Feature;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.StatsbeatModule;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.AzureMonitorHelper;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.PropertyHelper;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.ResourceParser;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.TempDirs;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.ThreadPoolUtils;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.VersionGenerator;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
Expand All @@ -47,6 +51,9 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;

import static java.util.concurrent.TimeUnit.DAYS;
Expand All @@ -61,6 +68,10 @@ class AzureMonitorExporterBuilder {
private static final String STATSBEAT_SHORT_INTERVAL_SECONDS_PROPERTY_NAME
= "STATSBEAT_SHORT_INTERVAL_SECONDS_PROPERTY_NAME";

private static final String SDKSTATS_DISABLED_ENV_VAR = "APPLICATIONINSIGHTS_SDKSTATS_DISABLED";
private static final String SDKSTATS_EXPORT_INTERVAL_ENV_VAR = "APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL";
private static final long SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS = 900; // 15 minutes

private static final Map<String, String> PROPERTIES
= CoreUtils.getProperties("azure-monitor-opentelemetry-exporter.properties");

Expand Down Expand Up @@ -93,13 +104,21 @@ void initializeIfNot(AzureMonitorAutoConfigureOptions exporterOptions, ConfigPro
this.spanDataMapper = createSpanDataMapper();
File tempDir = TempDirs.getApplicationInsightsTempDir(LOGGER,
"Telemetry will not be stored to disk and retried on sporadic network failures");
// Create customer-facing SDKStats if enabled (skip accumulator and listener when disabled)
boolean customerSdkStatsEnabled = isCustomerSdkStatsEnabled();
CustomerSdkStats customerSdkStats = customerSdkStatsEnabled ? createCustomerSdkStats() : null;
CustomerSdkStatsTelemetryPipelineListener customerSdkStatsListener
= customerSdkStats != null ? new CustomerSdkStatsTelemetryPipelineListener(customerSdkStats) : null;
// TODO (heya) change LocalStorageStats.noop() to statsbeatModule.getNonessentialStatsbeat() when we decide to collect non-essential Statsbeat by default.
this.builtTelemetryItemExporter = AzureMonitorHelper.createTelemetryItemExporter(httpPipeline, statsbeatModule,
tempDir, LocalStorageStats.noop());
tempDir, LocalStorageStats.noop(), customerSdkStatsListener);
if (LiveMetrics.isEnabled(configProperties)) {
this.quickPulse = createQuickPulse(resource);
}
startStatsbeatModule(statsbeatModule, configProperties, tempDir); // wait till TelemetryItemExporter has been initialized before starting StatsbeatModule
if (customerSdkStatsEnabled) {
startCustomerSdkStats(customerSdkStats, customerSdkStatsListener, resource);
Comment thread
rajkumar-rangaraj marked this conversation as resolved.
}
}

private QuickPulse createQuickPulse(Resource resource) {
Expand Down Expand Up @@ -240,6 +259,56 @@ private void startStatsbeatModule(StatsbeatModule statsbeatModule, ConfigPropert
false, initStatsbeatFeatures());
}

private CustomerSdkStats createCustomerSdkStats() {
// Use the raw library version number (e.g. "3.6.0") for the customer-facing stats dimension
String version = PropertyHelper.getSdkVersionNumber();
return CustomerSdkStats.create(version);
}
Comment thread
rajkumar-rangaraj marked this conversation as resolved.

private boolean isCustomerSdkStatsEnabled() {
String disabledValue = configProperties.getString(SDKSTATS_DISABLED_ENV_VAR);
if ("true".equalsIgnoreCase(disabledValue)) {
LOGGER.verbose("Customer SDKStats is disabled via configuration property {}.", SDKSTATS_DISABLED_ENV_VAR);
return false;
}
return true;
}

private void startCustomerSdkStats(CustomerSdkStats customerSdkStats,
CustomerSdkStatsTelemetryPipelineListener customerSdkStatsListener, Resource resource) {
// Get export interval from configuration or use default
long exportIntervalSeconds
= configProperties.getLong(SDKSTATS_EXPORT_INTERVAL_ENV_VAR, SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS);
if (exportIntervalSeconds <= 0) {
LOGGER.warning("Value for {} must be positive: {}. Using default {} seconds.",
SDKSTATS_EXPORT_INTERVAL_ENV_VAR, exportIntervalSeconds, SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS);
exportIntervalSeconds = SDKSTATS_DEFAULT_EXPORT_INTERVAL_SECONDS;
}

ConnectionString connectionString = getConnectionString();
String sdkVersion = VersionGenerator.getSdkVersion();
String cloudRole = resource.getAttribute(AttributeKey.stringKey("service.name"));
String cloudRoleInstance = resource.getAttribute(AttributeKey.stringKey("service.instance.id"));

ScheduledExecutorService customerSdkStatsExecutor = Executors
.newSingleThreadScheduledExecutor(ThreadPoolUtils.createDaemonThreadFactory(CustomerSdkStats.class));
customerSdkStatsListener.setScheduler(customerSdkStatsExecutor);

LOGGER.verbose("Customer SDKStats scheduler starting with interval {} seconds.", exportIntervalSeconds);
customerSdkStatsExecutor.scheduleWithFixedDelay(() -> {
try {
List<TelemetryItem> items
= customerSdkStats.collectAndReset(connectionString, sdkVersion, cloudRole, cloudRoleInstance);
if (!items.isEmpty()) {
LOGGER.verbose("Customer SDKStats: exporting {} metric items.", items.size());
builtTelemetryItemExporter.sendWithoutTracking(items);
}
} catch (RuntimeException e) {
LOGGER.warning("Error sending customer SDKStats", e);
}
}, exportIntervalSeconds, exportIntervalSeconds, TimeUnit.SECONDS);
}
Comment thread
rajkumar-rangaraj marked this conversation as resolved.

private HttpPipeline createStatsbeatHttpPipeline() {
if (exporterOptions.httpPipeline != null) {
return exporterOptions.httpPipeline;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.azure.monitor.opentelemetry.autoconfigure.implementation.logging.OperationLogger;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.ContextTagKeys;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.TelemetryItem;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.TelemetryBatchMetadata;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.AksResourceAttributes;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.IKeyMasker;
import io.opentelemetry.api.common.AttributeKey;
Expand Down Expand Up @@ -70,6 +71,33 @@ public CompletableResultCode send(List<TelemetryItem> telemetryItems) {
return CompletableResultCode.ofAll(resultCodeList);
}

/**
* Sends telemetry items without computing customer-facing SDKStats metadata.
* Used for sending customer SDKStats metrics themselves to prevent recursive counting.
* Items sent via this method will NOT trigger CustomerSdkStatsTelemetryPipelineListener
* since the TelemetryPipelineRequest will have empty item count maps.
*
* <p>This intentionally bypasses {@link #internalSendByBatch} and its AKS
* {@code _OTELRESOURCE_} metric injection, since SDKStats metrics are not
* application telemetry and should not carry OTel resource attributes.</p>
*/
public CompletableResultCode sendWithoutTracking(List<TelemetryItem> telemetryItems) {
Map<TelemetryItemBatchKey, List<TelemetryItem>> batches = splitIntoBatches(telemetryItems);
List<CompletableResultCode> resultCodeList = new ArrayList<>();
for (Map.Entry<TelemetryItemBatchKey, List<TelemetryItem>> batch : batches.entrySet()) {
try {
List<ByteBuffer> byteBuffers = serialize(batch.getValue());
encodeBatchOperationLogger.recordSuccess();
resultCodeList.add(telemetryPipeline.send(byteBuffers, batch.getKey().connectionString, listener));
} catch (Throwable t) {
encodeBatchOperationLogger.recordFailure(t.getMessage(), t);
resultCodeList.add(CompletableResultCode.ofFailure());
}
}
maybeAddToActiveExportResults(resultCodeList);
return CompletableResultCode.ofAll(resultCodeList);
}

// visible for tests
Map<TelemetryItemBatchKey, List<TelemetryItem>> splitIntoBatches(List<TelemetryItem> telemetryItems) {

Expand Down Expand Up @@ -109,6 +137,11 @@ public CompletableResultCode shutdown() {
CompletableResultCode internalSendByBatch(TelemetryItemBatchKey telemetryItemBatchKey,
List<TelemetryItem> telemetryItems) {
List<ByteBuffer> byteBuffers;

// Compute per-type item counts BEFORE injecting internal metrics (e.g. _OTELRESOURCE_)
// so that only customer telemetry is counted in SDKStats.
TelemetryBatchMetadata batchMetadata = TelemetryBatchMetadata.fromTelemetryItems(telemetryItems);

// Don't send _OTELRESOURCE_ custom metric when OTEL_RESOURCE_ATTRIBUTES env var is empty
// Don't send _OTELRESOURCE_ custom metric to Statsbeat yet
// Don't Send _OTELRESOURCE_ when the app is running on other env other than AKS
Expand All @@ -118,14 +151,15 @@ CompletableResultCode internalSendByBatch(TelemetryItemBatchKey telemetryItemBat
&& AksResourceAttributes.isAks(telemetryItemBatchKey.resource)) {
telemetryItems.add(0, createOtelResourceMetric(telemetryItemBatchKey));
}

try {
byteBuffers = serialize(telemetryItems);
encodeBatchOperationLogger.recordSuccess();
} catch (Throwable t) {
encodeBatchOperationLogger.recordFailure(t.getMessage(), t);
return CompletableResultCode.ofFailure();
}
return telemetryPipeline.send(byteBuffers, telemetryItemBatchKey.connectionString, listener);
return telemetryPipeline.send(byteBuffers, telemetryItemBatchKey.connectionString, listener, batchMetadata);
}

// serialize an array of TelemetryItems to an array of byte buffers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.tracing.Tracer;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.configuration.ConnectionString;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.TelemetryBatchMetadata;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.StatusCode;
import io.opentelemetry.sdk.common.CompletableResultCode;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -45,14 +46,19 @@ public TelemetryPipeline(HttpPipeline pipeline, Runnable statsbeatShutdown) {

public CompletableResultCode send(List<ByteBuffer> telemetry, String connectionString,
TelemetryPipelineListener listener) {
return send(telemetry, connectionString, listener, TelemetryBatchMetadata.empty());
}

public CompletableResultCode send(List<ByteBuffer> telemetry, String connectionString,
TelemetryPipelineListener listener, TelemetryBatchMetadata batchMetadata) {

ConnectionString connectionStringObj = ConnectionString.parse(connectionString);

URL url = redirectCache.computeIfAbsent(connectionString,
k -> getFullIngestionUrl(connectionStringObj.getIngestionEndpoint()));

TelemetryPipelineRequest request = new TelemetryPipelineRequest(url, connectionString,
connectionStringObj.getInstrumentationKey(), telemetry);
connectionStringObj.getInstrumentationKey(), telemetry, batchMetadata);

try {
CompletableResultCode result = new CompletableResultCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpRequest;
import com.azure.monitor.opentelemetry.autoconfigure.implementation.statsbeat.TelemetryBatchMetadata;
import reactor.core.publisher.Flux;

import java.net.URL;
Expand All @@ -20,13 +21,22 @@ public class TelemetryPipelineRequest {
private final List<ByteBuffer> byteBuffers;
private final int contentLength;

// Customer-facing SDKStats metadata
private final TelemetryBatchMetadata batchMetadata;

TelemetryPipelineRequest(URL url, String connectionString, String instrumentationKey,
List<ByteBuffer> byteBuffers) {
this(url, connectionString, instrumentationKey, byteBuffers, TelemetryBatchMetadata.empty());
}

public TelemetryPipelineRequest(URL url, String connectionString, String instrumentationKey,
List<ByteBuffer> byteBuffers, TelemetryBatchMetadata batchMetadata) {
this.url = url;
this.connectionString = connectionString;
this.instrumentationKey = instrumentationKey;
this.byteBuffers = byteBuffers;
contentLength = byteBuffers.stream().mapToInt(ByteBuffer::limit).sum();
this.batchMetadata = batchMetadata != null ? batchMetadata : TelemetryBatchMetadata.empty();
}
Comment thread
rajkumar-rangaraj marked this conversation as resolved.

public URL getUrl() {
Expand All @@ -50,6 +60,15 @@ public String getInstrumentationKey() {
return instrumentationKey;
}

/**
* Returns metadata about the telemetry batch, including item counts by type
* and success/failure breakdowns. Returns empty metadata for batches where
* item counting is not applicable (e.g. statsbeat, disk retries).
*/
public TelemetryBatchMetadata getTelemetryBatchMetadata() {
return batchMetadata;
}

HttpRequest createHttpRequest() {
HttpRequest request = new HttpRequest(HttpMethod.POST, url);
request.setBody(Flux.fromIterable(byteBuffers));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class TelemetryPipelineResponse {
private final int statusCode;
private final String body;

TelemetryPipelineResponse(int statusCode, String body) {
public TelemetryPipelineResponse(int statusCode, String body) {
this.statusCode = statusCode;
this.body = body;
}
Expand Down
Loading
Loading