diff --git a/ibm-mq-metrics/config.yml b/ibm-mq-metrics/config.yml index d0a3c8544..c1b91cca9 100644 --- a/ibm-mq-metrics/config.yml +++ b/ibm-mq-metrics/config.yml @@ -186,6 +186,8 @@ metrics: enabled: true "ibm.mq.heartbeat": # Queue manager heartbeat enabled: true + "ibm.mq.queue_manager.uptime": # Queue manager uptime + enabled: true "ibm.mq.archive.log.size": # Queue manager archive log size enabled: true "ibm.mq.manager.max.active.channels": # Queue manager max active channels diff --git a/ibm-mq-metrics/model/metrics.yaml b/ibm-mq-metrics/model/metrics.yaml index cfdfca3f2..e9cdeaa9c 100644 --- a/ibm-mq-metrics/model/metrics.yaml +++ b/ibm-mq-metrics/model/metrics.yaml @@ -533,6 +533,16 @@ groups: attributes: - ref: ibm.mq.queue.manager requirement_level: required + - id: ibm.mq.queue_manager.uptime + type: metric + metric_name: ibm.mq.queue_manager.uptime + stability: development + brief: "Queue manager uptime" + instrument: counter + unit: "s" + attributes: + - ref: ibm.mq.queue.manager + requirement_level: required - id: ibm.mq.archive.log.size type: metric metric_name: ibm.mq.archive.log.size diff --git a/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metrics/MetricProducer.java b/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metrics/MetricProducer.java index c76cc9496..e6860ef09 100644 --- a/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metrics/MetricProducer.java +++ b/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metrics/MetricProducer.java @@ -37,6 +37,7 @@ public final class MetricProducer implements io.opentelemetry.sdk.metrics.export private final Map counterIbmMqQueueDepthHighEvent; private final Map counterIbmMqQueueDepthLowEvent; private final Map counterIbmMqUnauthorizedEvent; + private final Map counterIbmMqQueueManagerUptime; private final Map counterIbmMqConnectionErrors; private long currentEpochNanos; @@ -50,6 +51,7 @@ public MetricProducer(Resource resource, InstrumentationScopeInfo info) { this.counterIbmMqQueueDepthHighEvent = new ConcurrentHashMap<>(); this.counterIbmMqQueueDepthLowEvent = new ConcurrentHashMap<>(); this.counterIbmMqUnauthorizedEvent = new ConcurrentHashMap<>(); + this.counterIbmMqQueueManagerUptime = new ConcurrentHashMap<>(); this.counterIbmMqConnectionErrors = new ConcurrentHashMap<>(); } @@ -668,6 +670,36 @@ public void recordIbmMqHeartbeat(long value, Attributes attributes) { this.currentEpochNanos, Clock.getDefault().now(), attributes, value))))); } + public void addIbmMqQueueManagerUptime(long value, Attributes attributes) { + long cumulativeValue = + this.counterIbmMqQueueManagerUptime.compute( + attributes, + (k, v) -> { + if (v == null) { + return value; + } else { + return v + value; + } + }); + metricData.add( + createMetricData( + this.resource, + this.instrumentationScopeInfo, + "ibm.mq.queue_manager.uptime", + "Queue manager uptime", + "s", + MetricDataType.LONG_SUM, + SumData.createLongSumData( + /* isMonotonic= */ true, + AggregationTemporality.CUMULATIVE, + Collections.singletonList( + LongPointData.create( + this.currentEpochNanos, + Clock.getDefault().now(), + attributes, + cumulativeValue))))); + } + public void recordIbmMqArchiveLogSize(long value, Attributes attributes) { metricData.add( createMetricData( diff --git a/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metrics/MetricsConfig.java b/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metrics/MetricsConfig.java index 2b4c09958..f2bc14c85 100644 --- a/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metrics/MetricsConfig.java +++ b/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metrics/MetricsConfig.java @@ -171,6 +171,10 @@ public boolean isIbmMqHeartbeatEnabled() { return isEnabled("ibm.mq.heartbeat"); } + public boolean isIbmMqQueueManagerUptimeEnabled() { + return isEnabled("ibm.mq.queue_manager.uptime"); + } + public boolean isIbmMqArchiveLogSizeEnabled() { return isEnabled("ibm.mq.archive.log.size"); } diff --git a/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metricscollector/MessageBuddy.java b/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metricscollector/MessageBuddy.java index 688f9541d..248190965 100644 --- a/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metricscollector/MessageBuddy.java +++ b/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metricscollector/MessageBuddy.java @@ -11,6 +11,8 @@ import com.ibm.mq.headers.pcf.PCFException; import com.ibm.mq.headers.pcf.PCFMessage; import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; public final class MessageBuddy { @@ -72,4 +74,29 @@ public static long channelStartTime(PCFMessage message) throws PCFException { public static String jobName(PCFMessage message) throws PCFException { return message.getStringParameterValue(CMQCFC.MQCACH_MCA_JOB_NAME).trim(); } + + /** + * Calculate the queue manager uptime in seconds. + * + *

Fetches the queue manager start date and time from the PCF response, parses them, and + * calculates the difference from the current system time. + * + * @param message the PCF response message containing queue manager information + * @return uptime in seconds since the queue manager started + * @throws PCFException if the required attributes cannot be retrieved from the PCF message + */ + public static long queueManagerUptime(PCFMessage message) throws PCFException { + String date = message.getStringParameterValue(CMQCFC.MQCACF_Q_MGR_START_DATE).trim(); + String time = message.getStringParameterValue(CMQCFC.MQCACF_Q_MGR_START_TIME).trim(); + + // Parse the date (format: yyyy-MM-dd) and time (format: HH.mm.ss) + // The queue manager start timestamp does not include timezone information in this PCF response, + // so we normalize to UTC for a stable and predictable baseline across regions. + LocalDateTime qmgrStartLocal = LocalDateTime.parse(date + "T" + time.replaceAll("\\.", ":")); + Instant qmgrStartInstant = qmgrStartLocal.toInstant(ZoneOffset.UTC); + Instant now = Instant.now(); + + // Calculate uptime in seconds + return now.getEpochSecond() - qmgrStartInstant.getEpochSecond(); + } } diff --git a/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metricscollector/QueueManagerMetricsCollector.java b/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metricscollector/QueueManagerMetricsCollector.java index 5f02d1ba6..61aa2dbe4 100644 --- a/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metricscollector/QueueManagerMetricsCollector.java +++ b/ibm-mq-metrics/src/main/java/io/opentelemetry/ibm/mq/metricscollector/QueueManagerMetricsCollector.java @@ -79,6 +79,17 @@ public void accept(MetricsCollectorContext context) { int maxActiveChannels = context.getQueueManager().getMaxActiveChannels(); producer.recordIbmMqManagerMaxActiveChannels(maxActiveChannels, attributes); } + if (context.getMetricsConfig().isIbmMqQueueManagerUptimeEnabled()) { + try { + long uptimeSeconds = MessageBuddy.queueManagerUptime(responses.get(0)); + producer.addIbmMqQueueManagerUptime(uptimeSeconds, attributes); + } catch (Exception e) { + logger.debug( + "Unable to calculate queue manager uptime for {}: {}", + context.getQueueManagerName(), + e.getMessage()); + } + } } catch (Exception e) { logger.error(e.getMessage()); throw new IllegalStateException(e); diff --git a/ibm-mq-metrics/src/test/java/io/opentelemetry/ibm/mq/metricscollector/MessageBuddyTest.java b/ibm-mq-metrics/src/test/java/io/opentelemetry/ibm/mq/metricscollector/MessageBuddyTest.java new file mode 100644 index 000000000..56615b403 --- /dev/null +++ b/ibm-mq-metrics/src/test/java/io/opentelemetry/ibm/mq/metricscollector/MessageBuddyTest.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.ibm.mq.metricscollector; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.mq.constants.CMQCFC; +import com.ibm.mq.headers.pcf.PCFException; +import com.ibm.mq.headers.pcf.PCFMessage; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.TimeZone; +import org.junit.jupiter.api.Test; + +class MessageBuddyTest { + + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ROOT); + private static final DateTimeFormatter TIME_FORMATTER = + DateTimeFormatter.ofPattern("HH.mm.ss", Locale.ROOT); + + @Test + void queueManagerUptimeUsesUtcAndDoesNotDriftWithSystemTimezone() throws PCFException { + TimeZone originalTz = TimeZone.getDefault(); + try { + Instant startInstant = Instant.now().minusSeconds(600); + LocalDateTime startUtc = LocalDateTime.ofInstant(startInstant, ZoneOffset.UTC); + String startDate = startUtc.format(DATE_FORMATTER); + String startTime = startUtc.format(TIME_FORMATTER); + + PCFMessage message = new PCFMessage(2, CMQCFC.MQCMD_INQUIRE_Q_MGR_STATUS, 1, true); + message.addParameter(CMQCFC.MQCACF_Q_MGR_START_DATE, startDate); + message.addParameter(CMQCFC.MQCACF_Q_MGR_START_TIME, startTime); + + TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Kiritimati")); + long beforeFirst = Instant.now().getEpochSecond(); + long firstResult = MessageBuddy.queueManagerUptime(message); + long afterFirst = Instant.now().getEpochSecond(); + + TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Honolulu")); + long beforeSecond = Instant.now().getEpochSecond(); + long secondResult = MessageBuddy.queueManagerUptime(message); + long afterSecond = Instant.now().getEpochSecond(); + + long expectedLowerFirst = beforeFirst - startInstant.getEpochSecond(); + long expectedUpperFirst = afterFirst - startInstant.getEpochSecond(); + assertThat(firstResult).isBetween(expectedLowerFirst, expectedUpperFirst); + + long expectedLowerSecond = beforeSecond - startInstant.getEpochSecond(); + long expectedUpperSecond = afterSecond - startInstant.getEpochSecond(); + assertThat(secondResult).isBetween(expectedLowerSecond, expectedUpperSecond); + + // Changing the JVM default timezone should not materially change uptime when UTC is used. + assertThat(Math.abs(firstResult - secondResult)).isLessThanOrEqualTo(2); + } finally { + TimeZone.setDefault(originalTz); + } + } +} diff --git a/ibm-mq-metrics/src/test/java/io/opentelemetry/ibm/mq/metricscollector/QueueManagerMetricsCollectorTest.java b/ibm-mq-metrics/src/test/java/io/opentelemetry/ibm/mq/metricscollector/QueueManagerMetricsCollectorTest.java index 1d3b2e79a..c4e2a6d72 100644 --- a/ibm-mq-metrics/src/test/java/io/opentelemetry/ibm/mq/metricscollector/QueueManagerMetricsCollectorTest.java +++ b/ibm-mq-metrics/src/test/java/io/opentelemetry/ibm/mq/metricscollector/QueueManagerMetricsCollectorTest.java @@ -57,10 +57,18 @@ void testProcessPCFRequestAndPublishQMetricsForInquireQStatusCmd() throws Except classUnderTest = new QueueManagerMetricsCollector(producer); classUnderTest.accept(context); List metricsList = new ArrayList<>(singletonList("ibm.mq.manager.status")); + metricsList.add("ibm.mq.queue_manager.uptime"); for (MetricData metric : producer.produce(Resource.empty())) { if (metricsList.remove(metric.getName())) { - assertThat(metric.getLongGaugeData().getPoints().iterator().next().getValue()).isEqualTo(2); + if ("ibm.mq.manager.status".equals(metric.getName())) { + assertThat(metric.getLongGaugeData().getPoints().iterator().next().getValue()) + .isEqualTo(2); + } + if ("ibm.mq.queue_manager.uptime".equals(metric.getName())) { + assertThat(metric.getLongSumData().getPoints().iterator().next().getValue()) + .isGreaterThan(0); + } } } assertThat(metricsList).isEmpty(); @@ -113,6 +121,8 @@ private static PCFMessage[] createPCFResponseForInquireQMgrStatusCmd() { response1.addParameter(CMQCFC.MQIACF_RESTART_LOG_SIZE, 42); response1.addParameter(CMQCFC.MQIACF_REUSABLE_LOG_SIZE, 42); response1.addParameter(CMQCFC.MQIACF_ARCHIVE_LOG_SIZE, 42); + response1.addParameter(CMQCFC.MQCACF_Q_MGR_START_DATE, "2024-01-01"); + response1.addParameter(CMQCFC.MQCACF_Q_MGR_START_TIME, "12.00.00"); return new PCFMessage[] {response1}; } diff --git a/ibm-mq-metrics/src/test/resources/conf/config.yml b/ibm-mq-metrics/src/test/resources/conf/config.yml index 07b177098..cf9f5a7ea 100644 --- a/ibm-mq-metrics/src/test/resources/conf/config.yml +++ b/ibm-mq-metrics/src/test/resources/conf/config.yml @@ -178,6 +178,8 @@ metrics: enabled: true "ibm.mq.heartbeat": # Queue manager heartbeat enabled: true + "ibm.mq.queue_manager.uptime": # Queue manager uptime + enabled: true "ibm.mq.archive.log.size": # Queue manager archive log size enabled: true "ibm.mq.manager.max.active.channels": # Queue manager max active channels