Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
bcee5f4
deprecate onboarding-enabler zutil, move to apiml-utility
Jan 14, 2026
0e0b568
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Jan 14, 2026
5243473
Merge remote-tracking branch 'origin' into reboot/feat/otel-standard
Jan 14, 2026
4fbb002
initial commit
Jan 14, 2026
1acb61c
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Jan 14, 2026
2d03079
wip add calculated attributes
Jan 16, 2026
976bb8b
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Jan 16, 2026
46fb08b
wip testing
Jan 16, 2026
7d12592
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Jan 16, 2026
1fa20d2
compile issue
Jan 16, 2026
06206ef
add defaults
Jan 16, 2026
d9d2541
otel test wip
Jan 16, 2026
d0e1967
test works
Jan 16, 2026
9fa1332
test data is empty
Jan 16, 2026
245a277
attempts to have acceptance tests working
Jan 21, 2026
a5e5b46
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Jan 21, 2026
a65086e
Merge branch 'v3.x.x' into reboot/feat/otel-standard
pablocarle Jan 22, 2026
014845a
InMemoryMetricReader for tests
richard-salac Jan 22, 2026
c42575e
rename test, update structure
Jan 22, 2026
07054ca
add unit tests
Jan 23, 2026
f6dfe77
wip
Jan 23, 2026
92f6300
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Jan 23, 2026
c342fc4
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Jan 27, 2026
94c9370
fix for acceptance test
Jan 27, 2026
753c7c1
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Jan 27, 2026
f4c4416
Merge branch 'v3.x.x' into reboot/feat/otel-standard
pablocarle Jan 27, 2026
1015c38
unit test
Jan 27, 2026
1e2b2fc
Merge branch 'v3.x.x' into reboot/feat/otel-standard
pablocarle Jan 27, 2026
a8be34c
complete functional test
Jan 27, 2026
86094cb
remove standard attribute from test
Jan 27, 2026
8dd9c79
Merge branch 'v3.x.x' into reboot/feat/otel-standard
pablocarle Jan 27, 2026
49520e1
attempt to fix otel settings start.sh
Jan 28, 2026
68680c3
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Jan 29, 2026
a667103
add unit test
Jan 29, 2026
ba4e739
fix merge issue
Jan 30, 2026
f80fba5
Merge branch 'v3.x.x' into reboot/feat/otel-standard
pablocarle Jan 30, 2026
c7c98ba
Merge branch 'v3.x.x' into reboot/feat/otel-standard
pablocarle Feb 2, 2026
ed9c5c3
Merge branch 'v3.x.x' into reboot/feat/otel-standard
richard-salac Feb 2, 2026
ca9d625
pr review 1
Feb 3, 2026
05b870d
use attributes, add exporter settings
Feb 3, 2026
80874a4
fix for blank otel properties in start.sh
Feb 3, 2026
5c70cf5
add zos attributes
Feb 3, 2026
0a4a818
Merge remote-tracking branch 'origin/v3.x.x' into reboot/feat/otel-st…
Feb 4, 2026
1991a26
add comment
Feb 4, 2026
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
1 change: 1 addition & 0 deletions apiml-common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies {
implementation libs.spring.boot.starter.actuator
implementation libs.spring.boot.starter.web
implementation libs.spring.cloud.starter.eureka.client
implementation libs.opentelemetry.spring.boot.starter
compileOnly libs.netty.reactor.http
implementation libs.eureka.core

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.product.opentelemetry;

import io.opentelemetry.api.common.Attributes;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;

@ConditionalOnExpression("#{!T(org.zowe.apiml.product.zos.ZosSystemInformation).isRunningOnZos()}")
@Component
public class ApimlNonZosOpenTelemetryResourceProvider extends ApimlOpenTelemetryResourceProvider {

@Override
@Nonnull
public Attributes calculateAttributes() {
return Attributes.empty();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.product.opentelemetry;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
import io.opentelemetry.sdk.resources.Resource;

import javax.annotation.Nonnull;

public abstract class ApimlOpenTelemetryResourceProvider implements ResourceProvider {

public abstract @Nonnull Attributes calculateAttributes();

@Override
public Resource createResource(@Nonnull ConfigProperties config) {
var attributesBuilder = Attributes.builder();

attributesBuilder.putAll(calculateAttributes());
return Resource.create(attributesBuilder.build());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.product.opentelemetry;

import io.opentelemetry.api.common.Attributes;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Component;
import org.zowe.apiml.product.zos.ZosSystemInformation;

import javax.annotation.Nonnull;

import java.util.Map;
import java.util.Optional;

import static org.zowe.apiml.product.zos.ZosSystemInformation.ZOS_JOB_ID;
import static org.zowe.apiml.product.zos.ZosSystemInformation.ZOS_JOB_NAME;
import static org.zowe.apiml.product.zos.ZosSystemInformation.ZOS_SYSCLONE;
import static org.zowe.apiml.product.zos.ZosSystemInformation.ZOS_SYSNAME;
import static org.zowe.apiml.product.zos.ZosSystemInformation.ZOS_SYSPLEX;
import static org.zowe.apiml.product.zos.ZosSystemInformation.ZOS_USER_ID;

@Component
@RequiredArgsConstructor
@ConditionalOnMissingBean(ApimlNonZosOpenTelemetryResourceProvider.class)
@Slf4j
public class ApimlZosOpenTelemetryResourceProvider extends ApimlOpenTelemetryResourceProvider {

private final ZosSystemInformation zosSystemInformation;

@Value("${otel.resource.attributes.deployment.environment.name:#{null}}")
private String environmentName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it's a matter of deciding if it should be part of the default namespace for instance. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove it form here.


@Value("${otel.resource.attributes.service.namespace:#{null}}")
private String serviceNamespace;

@Value("${otel.resource.attributes.service.name:#{null}}")
private String serviceName;

@Value("${apiml.service.apimlId:#{null}}")
private String apimlId;

@Value("${apiml.service.port:10010}")
private int port;

@Value("${otel.resource.attributes.zos.sysplex.name:#{null}}")
private String sysplexName;

@Value("${otel.resource.attributes.mainframe.lpar.name:#{null}}")
private String lparName;

@Value("${otel.resource.attributes.zos.smf.id:#{null}}")
private String smfId;

@PostConstruct
void afterPropertiesSet() {
log.debug("Using ZOS OpenTelemetry resource provider");
}

@SuppressWarnings("null")
@Override
@Nonnull
public Attributes calculateAttributes() {
var attributesBuilder = Attributes.builder();

var zosAttributes = zosSystemInformation.get();

if (StringUtils.isBlank(serviceNamespace)) {
var generatedDefaultNamespace = generateServiceNamespace(zosAttributes);
attributesBuilder.put("service.namespace", generatedDefaultNamespace);
log.debug("service.namespace not provided in configuration, using generated default {}", generatedDefaultNamespace);
}

if (StringUtils.isBlank(serviceName)) {
var generatedServiceName = generateServiceName(zosAttributes);
attributesBuilder.put("service.name", generatedServiceName);
log.debug("service.name not provided in configuration, using generated default {}", generatedServiceName);
}

if (StringUtils.isBlank(sysplexName)) {
var sysplexName = zosAttributes.get(ZOS_SYSPLEX);
if (sysplexName != null && StringUtils.isNotBlank(sysplexName.toString())) {
log.debug("zos.sysplex.name not provided in configuration, using system-obtained {}", sysplexName);
} else {
log.debug("zos.sysplex.name not provided in configuration. Could not determine name from system");
}
}

if (StringUtils.isBlank(lparName)) {
var lparName = zosAttributes.get(ZOS_SYSNAME);
if (lparName != null && StringUtils.isNotBlank(lparName.toString())) {
log.debug("mainframe.lpar.name not provided in configuration, using system-obtained {}", lparName);
} else {
log.debug("mainframe.lpar.name not provided in configuration. Could not determine name from system");
}
}

if (StringUtils.isBlank(smfId)) {
var smfId = zosAttributes.get(ZOS_SYSCLONE);
if (smfId != null && StringUtils.isNotBlank(smfId.toString())) {
log.debug("zos.smf.id not provided in configuration, using system-obtained {}", smfId);
} else {
log.debug("zos.smf.id not provided in configuration. Could not determine ID from system");
}
}
Comment on lines +92 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are checked for value but never set to the attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these only to show in the logs if such overrides are happening, if there's no value in the logs we can remove it. We don't need to explicitly set them because they are otel.* properties which are automatically picked up by OpenTelemetry implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless it is configured or discovered, they are not set in resource attributes and are not present in the exported data.


Optional.ofNullable(zosAttributes.get(ZOS_JOB_ID))
.map(String::valueOf)
.filter(StringUtils::isNotBlank)
.ifPresent(zosJobId -> attributesBuilder.put(ZosAttributes.ZOS_JOBID, zosJobId));
Comment on lines +119 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant naming is confusing: ZOS_JOB_ID vs ZOS_JOBID
and similarly for some others

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can rename them, but they are different values anyway, one is zos.jobid from the system information and the other one is process.zos.jobid for the target OpenTelemetry attribute

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is confusing, as you say they mean something totally different and yet named so similarly. Maybe "OTEL_ZOS_JOBNAME" or "OtelZosAttributes.ZOS_JOBNAME"?


Optional.ofNullable(zosAttributes.get(ZOS_JOB_NAME))
.map(String::valueOf)
.filter(StringUtils::isNotBlank)
.ifPresent(zosJobName -> attributesBuilder.put(ZosAttributes.ZOS_JOBNAME, zosJobName));

Optional.ofNullable(zosAttributes.get(ZOS_USER_ID))
.map(String::valueOf)
.filter(StringUtils::isNotBlank)
.ifPresent(zosUserId -> attributesBuilder.put(ZosAttributes.ZOS_USERID, zosUserId));

return attributesBuilder.build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing the service.instance.id fallback. Or are we ok with the generated uuid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we were ok with the generated one. If we go the fallback route, we need to define some boolean to let the user decide.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I got the impression that you prefer the lpar+port over the uuid.

}

private String generateServiceName(Map<String,Object> zosAttributes) {
var systemName = StringUtils.isBlank(apimlId) ? zosAttributes.get(ZOS_SYSPLEX) : apimlId;

return systemName + ":" + port;
}

private String generateServiceNamespace(Map<String,Object> zosAttributes) {
return "apiml:" + generateServiceName(zosAttributes);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this bring additional value over having only the service.name if no namespace is provided?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is simply my suggestion to have defaults that make sense. It's open to suggestions. I expect most users will set these two values.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense when it is only service name with a prefix. Then it has no additional value over the service name in therm of identifying the installation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default namespace should be more of a corner case rather than the norm. We should aim at users configuring a namespace always with meaning in the configuration if they enable OpenTelemetry.
I agree it doesn't have value as it is over the service name, but we need to have a default nonetheless.
How about adding the environmentName value as a prefix?

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.product.opentelemetry;

/**
* Set of attributes set by API ML
*/
public class ZosAttributes {

public static final String ZOS_JOBNAME = "process.zos.jobname";
public static final String ZOS_USERID = "process.zos.userid";
public static final String ZOS_JOBID = "process.zos.jobid";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.product.opentelemetry;

import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;

@ExtendWith(MockitoExtension.class)
class ApimlNonZosOpenTelemetryResourceProviderTest {

private ApimlNonZosOpenTelemetryResourceProvider resourceProvider;

@BeforeEach
void setUp() {
this.resourceProvider = new ApimlNonZosOpenTelemetryResourceProvider();
}

@Test
void testCalculateAttributes() {
var result = resourceProvider.calculateAttributes();
assertTrue(result.isEmpty());
}

@Test
void testCreateResource() {
var result = resourceProvider.createResource(mock(ConfigProperties.class));
assertTrue(result.getAttributes().isEmpty());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.product.opentelemetry;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.zowe.apiml.product.zos.ZosSystemInformation;

import java.util.Map;

import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class ApimlZosOpenTelemetryResourceProviderTest {

@Mock
private ZosSystemInformation zosSystemInformation;

private ApimlZosOpenTelemetryResourceProvider resourceProvider;

@BeforeEach
void setUp() {
resourceProvider = new ApimlZosOpenTelemetryResourceProvider(zosSystemInformation);
ReflectionTestUtils.setField(resourceProvider, "port", 10010);
}

@Nested
class GivenZosAttributes {

@Test
void testCalculateAttributes() {
when(zosSystemInformation.get()).thenReturn(Map.of(
"zos.jobid", "JOB12345",
"zos.jobname", "JOBN12",
"zos.userid", "ZWEUSR",
"zos.pid", 123456,
"zos.sysname", "SYSA",
"zos.sysclone", "16",
"zos.sysplex", "PLEX1"
));
var attributes = resourceProvider.calculateAttributes();

assertFalse(attributes.isEmpty());
assertNull(attributes.get(stringKey("mainframe.lpar.name")));

assertEquals("JOB12345", attributes.get(stringKey("process.zos.jobid")));
assertEquals("JOBN12", attributes.get(stringKey("process.zos.jobname")));
assertEquals("ZWEUSR", attributes.get(stringKey("process.zos.userid")));
assertEquals("apiml:PLEX1:10010", attributes.get(stringKey("service.namespace")));
assertEquals("PLEX1:10010", attributes.get(stringKey("service.name")));
}

}

}
Loading
Loading