Skip to content

Commit 08e0ab4

Browse files
Merge pull request #171 from OpenElements/interceptors
Metrics support for Spring started
2 parents 554ebfc + ccf60c8 commit 08e0ab4

File tree

9 files changed

+202
-17
lines changed

9 files changed

+202
-17
lines changed

hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/ProtocolLayerClientImpl.java

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@
2323
import com.hedera.hashgraph.sdk.Query;
2424
import com.hedera.hashgraph.sdk.SubscriptionHandle;
2525
import com.hedera.hashgraph.sdk.TokenAssociateTransaction;
26-
import com.hedera.hashgraph.sdk.TokenDissociateTransaction;
2726
import com.hedera.hashgraph.sdk.TokenBurnTransaction;
2827
import com.hedera.hashgraph.sdk.TokenCreateTransaction;
28+
import com.hedera.hashgraph.sdk.TokenDissociateTransaction;
2929
import com.hedera.hashgraph.sdk.TokenMintTransaction;
3030
import com.hedera.hashgraph.sdk.TopicCreateTransaction;
31-
import com.hedera.hashgraph.sdk.TopicUpdateTransaction;
3231
import com.hedera.hashgraph.sdk.TopicDeleteTransaction;
3332
import com.hedera.hashgraph.sdk.TopicMessageQuery;
3433
import com.hedera.hashgraph.sdk.TopicMessageSubmitTransaction;
34+
import com.hedera.hashgraph.sdk.TopicUpdateTransaction;
3535
import com.hedera.hashgraph.sdk.Transaction;
3636
import com.hedera.hashgraph.sdk.TransactionReceipt;
3737
import com.hedera.hashgraph.sdk.TransactionRecord;
@@ -41,6 +41,10 @@
4141
import com.openelements.hiero.base.HieroException;
4242
import com.openelements.hiero.base.data.Account;
4343
import com.openelements.hiero.base.data.ContractParam;
44+
import com.openelements.hiero.base.interceptors.ReceiveRecordInterceptor;
45+
import com.openelements.hiero.base.interceptors.ReceiveRecordInterceptor.ReceiveRecordHandler;
46+
import com.openelements.hiero.base.protocol.ProtocolLayerClient;
47+
import com.openelements.hiero.base.protocol.TransactionListener;
4448
import com.openelements.hiero.base.protocol.data.AccountBalanceRequest;
4549
import com.openelements.hiero.base.protocol.data.AccountBalanceResponse;
4650
import com.openelements.hiero.base.protocol.data.AccountCreateRequest;
@@ -65,34 +69,33 @@
6569
import com.openelements.hiero.base.protocol.data.FileInfoResponse;
6670
import com.openelements.hiero.base.protocol.data.FileUpdateRequest;
6771
import com.openelements.hiero.base.protocol.data.FileUpdateResult;
68-
import com.openelements.hiero.base.protocol.ProtocolLayerClient;
6972
import com.openelements.hiero.base.protocol.data.TokenAssociateRequest;
7073
import com.openelements.hiero.base.protocol.data.TokenAssociateResult;
71-
import com.openelements.hiero.base.protocol.data.TokenDissociateRequest;
72-
import com.openelements.hiero.base.protocol.data.TokenDissociateResult;
7374
import com.openelements.hiero.base.protocol.data.TokenBurnRequest;
7475
import com.openelements.hiero.base.protocol.data.TokenBurnResult;
7576
import com.openelements.hiero.base.protocol.data.TokenCreateRequest;
7677
import com.openelements.hiero.base.protocol.data.TokenCreateResult;
78+
import com.openelements.hiero.base.protocol.data.TokenDissociateRequest;
79+
import com.openelements.hiero.base.protocol.data.TokenDissociateResult;
7780
import com.openelements.hiero.base.protocol.data.TokenMintRequest;
7881
import com.openelements.hiero.base.protocol.data.TokenMintResult;
7982
import com.openelements.hiero.base.protocol.data.TokenTransferRequest;
8083
import com.openelements.hiero.base.protocol.data.TokenTransferResult;
8184
import com.openelements.hiero.base.protocol.data.TopicCreateRequest;
8285
import com.openelements.hiero.base.protocol.data.TopicCreateResult;
83-
import com.openelements.hiero.base.protocol.data.TopicUpdateRequest;
84-
import com.openelements.hiero.base.protocol.data.TopicUpdateResult;
8586
import com.openelements.hiero.base.protocol.data.TopicDeleteRequest;
8687
import com.openelements.hiero.base.protocol.data.TopicDeleteResult;
8788
import com.openelements.hiero.base.protocol.data.TopicMessageRequest;
8889
import com.openelements.hiero.base.protocol.data.TopicMessageResult;
8990
import com.openelements.hiero.base.protocol.data.TopicSubmitMessageRequest;
9091
import com.openelements.hiero.base.protocol.data.TopicSubmitMessageResult;
91-
import com.openelements.hiero.base.protocol.TransactionListener;
92+
import com.openelements.hiero.base.protocol.data.TopicUpdateRequest;
93+
import com.openelements.hiero.base.protocol.data.TopicUpdateResult;
9294
import com.openelements.hiero.base.protocol.data.TransactionType;
9395
import java.util.List;
9496
import java.util.Objects;
9597
import java.util.concurrent.CopyOnWriteArrayList;
98+
import java.util.concurrent.atomic.AtomicReference;
9699
import java.util.function.Consumer;
97100
import org.jspecify.annotations.NonNull;
98101
import org.slf4j.Logger;
@@ -108,11 +111,20 @@ public class ProtocolLayerClientImpl implements ProtocolLayerClient {
108111

109112
private final HieroContext hieroContext;
110113

114+
private final AtomicReference<ReceiveRecordInterceptor> recordInterceptor = new AtomicReference<>(
115+
ReceiveRecordInterceptor.DEFAULT_INTERCEPTOR);
116+
111117
public ProtocolLayerClientImpl(@NonNull final HieroContext hieroContext) {
112118
this.hieroContext = Objects.requireNonNull(hieroContext, "hieroContext must not be null");
113119
listeners = new CopyOnWriteArrayList<>();
114120
}
115121

122+
public void setRecordInterceptor(
123+
@NonNull final ReceiveRecordInterceptor recordInterceptor) {
124+
Objects.requireNonNull(recordInterceptor, "recordInterceptor must not be null");
125+
this.recordInterceptor.set(recordInterceptor);
126+
}
127+
116128
@Override
117129
public AccountBalanceResponse executeAccountBalanceQuery(@NonNull final AccountBalanceRequest request)
118130
throws HieroException {
@@ -618,7 +630,10 @@ private <T extends Transaction<T>> TransactionRecord executeTransactionAndWaitOn
618630
try {
619631
log.debug("Waiting for record of transaction '{}' of type {}", receipt.transactionId,
620632
transaction.getClass().getSimpleName());
621-
return receipt.transactionId.getRecord(hieroContext.getClient());
633+
634+
final ReceiveRecordHandler data = new ReceiveRecordHandler(transaction, receipt,
635+
r -> r.transactionId.getRecord(hieroContext.getClient()));
636+
return recordInterceptor.get().getRecordFor(data);
622637
} catch (final Exception e) {
623638
throw new HieroException("Failed to receive record of transaction '" + receipt.transactionId + "' of type "
624639
+ transaction.getClass(), e);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.openelements.hiero.base.interceptors;
2+
3+
import com.hedera.hashgraph.sdk.Transaction;
4+
import com.hedera.hashgraph.sdk.TransactionReceipt;
5+
import com.hedera.hashgraph.sdk.TransactionRecord;
6+
import java.util.Objects;
7+
import org.jspecify.annotations.NonNull;
8+
9+
/**
10+
* First simple interceptor for receiving a record. This interceptor is used to intercept the call for receiving a
11+
* record for a transaction. Frameworks like Spring can use this interceptor to add functionalities like metrics,
12+
* tracing, or logging to the calls.
13+
*/
14+
@FunctionalInterface
15+
public interface ReceiveRecordInterceptor {
16+
17+
/**
18+
* Default interceptor that does nothing.
19+
*/
20+
ReceiveRecordInterceptor DEFAULT_INTERCEPTOR = data -> data.handle();
21+
22+
/**
23+
* Intercept the call for receiving a record for a transaction.
24+
*
25+
* @param handler the handler that will be used to receive the record
26+
* @return the record for the transaction
27+
* @throws Exception if the interceptor fails
28+
*/
29+
@NonNull
30+
TransactionRecord getRecordFor(@NonNull ReceiveRecordHandler handler) throws Exception;
31+
32+
/**
33+
* Handler for receiving a record for a transaction.
34+
*
35+
* @param transaction the transaction for which the record is received
36+
* @param receipt the receipt for the transaction
37+
* @param function the function that will be used to receive the record
38+
*/
39+
record ReceiveRecordHandler(@NonNull Transaction transaction, @NonNull TransactionReceipt receipt,
40+
@NonNull ReceiveRecordFunction function) {
41+
42+
public ReceiveRecordHandler {
43+
Objects.requireNonNull(transaction, "transaction must not be null");
44+
Objects.requireNonNull(receipt, "receipt must not be null");
45+
Objects.requireNonNull(function, "handler must not be null");
46+
}
47+
48+
/**
49+
* Handle the call for receiving a record for a transaction.
50+
*
51+
* @return the record for the transaction
52+
* @throws Exception if the interceptor fails
53+
*/
54+
@NonNull
55+
public TransactionRecord handle() throws Exception {
56+
return function.handle(receipt);
57+
}
58+
}
59+
60+
/**
61+
* Function that will be used to receive the record for a transaction.
62+
*/
63+
@FunctionalInterface
64+
interface ReceiveRecordFunction {
65+
66+
/**
67+
* Handle the call for receiving a record for a transaction.
68+
*
69+
* @param receipt the receipt for the transaction
70+
* @return the record for the transaction
71+
* @throws Exception if the interceptor fails
72+
*/
73+
@NonNull
74+
TransactionRecord handle(@NonNull TransactionReceipt receipt) throws Exception;
75+
}
76+
}

hiero-enterprise-base/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
exports com.openelements.hiero.base.implementation.data to com.openelements.hiero.base.test;
1010
exports com.openelements.hiero.base.config.implementation;
1111
exports com.openelements.hiero.base.protocol.data;
12+
exports com.openelements.hiero.base.interceptors to com.openelements.hiero.base.test;
1213

1314
uses com.openelements.hiero.base.config.NetworkSettingsProvider;
1415
provides com.openelements.hiero.base.config.NetworkSettingsProvider with com.openelements.hiero.base.config.hedera.HederaNetworkSettingsProvider;

hiero-enterprise-spring-sample/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@
2222
<groupId>org.springframework.boot</groupId>
2323
<artifactId>spring-boot-starter-web</artifactId>
2424
</dependency>
25+
<dependency>
26+
<groupId>org.springframework.boot</groupId>
27+
<artifactId>spring-boot-starter-actuator</artifactId>
28+
</dependency>
2529
<dependency>
2630
<groupId>${project.groupId}</groupId>
2731
<artifactId>hiero-enterprise-spring</artifactId>
2832
</dependency>
33+
<dependency>
34+
<groupId>io.micrometer</groupId>
35+
<artifactId>micrometer-registry-prometheus</artifactId>
36+
</dependency>
2937
<dependency>
3038
<groupId>io.grpc</groupId>
3139
<artifactId>grpc-okhttp</artifactId>

hiero-enterprise-spring-sample/src/main/java/com/openelements/hiero/spring/sample/Application.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package com.openelements.hiero.spring.sample;
22

33
import com.openelements.hiero.spring.EnableHiero;
4+
import io.micrometer.core.instrument.MeterRegistry;
5+
import org.springframework.beans.factory.annotation.Autowired;
46
import org.springframework.boot.SpringApplication;
57
import org.springframework.boot.autoconfigure.SpringBootApplication;
68

79
@SpringBootApplication
810
@EnableHiero
911
public class Application {
1012

13+
@Autowired
14+
MeterRegistry registry;
15+
1116
public static void main(String[] args) {
1217
SpringApplication.run(Application.class, args);
1318
}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
spring.config.import=optional:file:.env[.properties]
2-
32
spring.hiero.accountId=${HEDERA_ACCOUNT_ID:0.0.123}
43
spring.hiero.privateKey=${HEDERA_PRIVATE_KEY}
5-
spring.hiero.network.name=${HEDERA_NETWORK:testnet}
6-
4+
spring.hiero.network.name=${HEDERA_NETWORK:hedera-testnet}
75
logging.level.com.openelements=DEBUG
86
logging.pattern.console=%d{HH:mm:ss.SSS} %logger{36} - %msg%n
7+
server.port=${SERVER_PORT:8080}
8+
# Needs to be done to activate the actuator endpoints (for metrics)
9+
management.endpoints.web.exposure.include=*

hiero-enterprise-spring/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
<artifactId>spring-boot-configuration-processor</artifactId>
4444
<optional>true</optional>
4545
</dependency>
46+
<dependency>
47+
<groupId>io.micrometer</groupId>
48+
<artifactId>micrometer-core</artifactId>
49+
<optional>true</optional>
50+
</dependency>
4651
<dependency>
4752
<groupId>${project.groupId}</groupId>
4853
<artifactId>hiero-enterprise-test</artifactId>

hiero-enterprise-spring/src/main/java/com/openelements/hiero/spring/implementation/HieroAutoConfiguration.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,41 @@
1414
import com.openelements.hiero.base.implementation.FungibleTokenClientImpl;
1515
import com.openelements.hiero.base.implementation.NetworkRepositoryImpl;
1616
import com.openelements.hiero.base.implementation.NftClientImpl;
17-
import com.openelements.hiero.base.implementation.TopicClientImpl;
1817
import com.openelements.hiero.base.implementation.NftRepositoryImpl;
19-
import com.openelements.hiero.base.implementation.TopicRepositoryImpl;
2018
import com.openelements.hiero.base.implementation.ProtocolLayerClientImpl;
2119
import com.openelements.hiero.base.implementation.SmartContractClientImpl;
2220
import com.openelements.hiero.base.implementation.TokenRepositoryImpl;
21+
import com.openelements.hiero.base.implementation.TopicClientImpl;
22+
import com.openelements.hiero.base.implementation.TopicRepositoryImpl;
2323
import com.openelements.hiero.base.implementation.TransactionRepositoryImpl;
24+
import com.openelements.hiero.base.interceptors.ReceiveRecordInterceptor;
2425
import com.openelements.hiero.base.mirrornode.AccountRepository;
2526
import com.openelements.hiero.base.mirrornode.MirrorNodeClient;
2627
import com.openelements.hiero.base.mirrornode.NetworkRepository;
2728
import com.openelements.hiero.base.mirrornode.NftRepository;
2829
import com.openelements.hiero.base.mirrornode.TokenRepository;
29-
import com.openelements.hiero.base.mirrornode.TransactionRepository;
3030
import com.openelements.hiero.base.mirrornode.TopicRepository;
31+
import com.openelements.hiero.base.mirrornode.TransactionRepository;
3132
import com.openelements.hiero.base.protocol.ProtocolLayerClient;
3233
import com.openelements.hiero.base.verification.ContractVerificationClient;
3334
import java.net.URI;
3435
import java.net.URL;
3536
import java.util.List;
3637
import org.slf4j.Logger;
3738
import org.slf4j.LoggerFactory;
39+
import org.springframework.beans.factory.annotation.Autowired;
3840
import org.springframework.boot.autoconfigure.AutoConfiguration;
41+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3942
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
4043
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4144
import org.springframework.context.annotation.Bean;
45+
import org.springframework.context.annotation.Import;
4246
import org.springframework.web.client.RestClient;
4347
import org.springframework.web.context.annotation.ApplicationScope;
4448

4549
@AutoConfiguration
4650
@EnableConfigurationProperties({HieroProperties.class, HieroNetworkProperties.class})
51+
@Import({MicrometerSupportConfig.class})
4752
public class HieroAutoConfiguration {
4853

4954
private static final Logger log = LoggerFactory.getLogger(HieroAutoConfiguration.class);
@@ -61,8 +66,13 @@ HieroContext hieroContext(final HieroConfig hieroConfig) {
6166
}
6267

6368
@Bean
64-
ProtocolLayerClient protocolLevelClient(final HieroContext hieroContext) {
65-
return new ProtocolLayerClientImpl(hieroContext);
69+
ProtocolLayerClient protocolLevelClient(final HieroContext hieroContext,
70+
@Autowired(required = false) final ReceiveRecordInterceptor interceptor) {
71+
ProtocolLayerClientImpl protocolLayerClient = new ProtocolLayerClientImpl(hieroContext);
72+
if (interceptor != null) {
73+
protocolLayerClient.setRecordInterceptor(interceptor);
74+
}
75+
return protocolLayerClient;
6676
}
6777

6878
@Bean
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.openelements.hiero.spring.implementation;
2+
3+
import com.hedera.hashgraph.sdk.ContractExecuteTransaction;
4+
import com.hedera.hashgraph.sdk.TransactionRecord;
5+
import com.openelements.hiero.base.interceptors.ReceiveRecordInterceptor;
6+
import io.micrometer.core.instrument.Counter;
7+
import io.micrometer.core.instrument.MeterRegistry;
8+
import io.micrometer.core.instrument.Tag;
9+
import io.micrometer.core.instrument.Timer;
10+
import java.util.HashSet;
11+
import java.util.Set;
12+
import org.jspecify.annotations.NonNull;
13+
import org.springframework.boot.autoconfigure.AutoConfiguration;
14+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
15+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
16+
import org.springframework.context.annotation.Bean;
17+
18+
/**
19+
* Micrometer support for Hiero. This configuration class is used to create a {@link ReceiveRecordInterceptor} that will
20+
* measure metrics for Hiero transactions. The config is only loaded if the {@code spring.hiero.metrics.enabled}
21+
* property is set to {@code true} or not set at all. Next to that, the {@code MetricsAutoConfiguration} configuration
22+
* must be on the classpath.
23+
*/
24+
@AutoConfiguration
25+
@ConditionalOnProperty(name = "spring.hiero.metrics.enabled", havingValue = "true", matchIfMissing = true)
26+
@ConditionalOnClass(name = "org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration")
27+
public class MicrometerSupportConfig {
28+
29+
public static final String TRANSACTION_TYPE_TAG = "hiero.transaction.record.type";
30+
public static final String CONTRACT_ID_TAG = "hiero.transaction.record.contractId";
31+
public static final String TIMER_NAME = "hiero.transaction.record.time";
32+
public static final String COUNTER_NAME = "hiero.transaction.record";
33+
34+
/**
35+
* Creates a {@link ReceiveRecordInterceptor} that will measure metrics for Hiero transactions.
36+
*
37+
* @param meterRegistry the {@link MeterRegistry} to use for metrics
38+
* @return the {@link ReceiveRecordInterceptor} to use for metrics
39+
*/
40+
@Bean
41+
@NonNull
42+
public ReceiveRecordInterceptor interceptRecordReceive(@NonNull final MeterRegistry meterRegistry) {
43+
return handler -> {
44+
final String transactionType = handler.transaction().getClass().getSimpleName();
45+
final Set<Tag> tags = new HashSet<>();
46+
tags.add(Tag.of(TRANSACTION_TYPE_TAG, transactionType));
47+
if (handler.transaction() instanceof ContractExecuteTransaction contractExecuteTransaction) {
48+
tags.add(Tag.of(CONTRACT_ID_TAG,
49+
contractExecuteTransaction.getContractId().toString()));
50+
}
51+
final Timer timer = meterRegistry.timer(TIMER_NAME, tags);
52+
final Counter counter = meterRegistry.counter(COUNTER_NAME, tags);
53+
return timer.record(() -> {
54+
try {
55+
final TransactionRecord transactionRecord = handler.handle();
56+
counter.increment();
57+
return transactionRecord;
58+
} catch (Exception e) {
59+
throw new RuntimeException("Error in handling record interceptor", e);
60+
}
61+
});
62+
};
63+
}
64+
}

0 commit comments

Comments
 (0)