@@ -51,6 +51,282 @@ NOTE: Be careful that you don't mix up entities retrieved from one database with
51
51
The database name is requested for each new transaction, so you might end up with less or more entities than expected when changing the database name in between calls.
52
52
Or worse, you could inevitably store the wrong entities in the wrong database.
53
53
54
+ [[faq.multidatabase.health]]
55
+ === The Spring Boot Neo4j health indicator targets the default database, how can I change that?
56
+
57
+ Spring Boot comes with both imperative and reactive Neo4j https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-health[health indicators.]
58
+ Both variants are able to detect multiple beans of `org.neo4j.driver.Driver` inside the application context and provide
59
+ a contribution to the overall health for each instance.
60
+ The Neo4j driver however does connect to a server and not to a specific database inside that server.
61
+ Spring Boot is able to configure the driver without Spring Data Neo4j and as the information which database is to be used
62
+ is tied to Spring Data Neo4j, this information is not available to the built-in health indicator.
63
+
64
+ This is most likely not a problem in many deployment scenarios.
65
+ However, if configured database user does not have at least access rights to the default database, the health checks will fail.
66
+
67
+ This can be mitigated by custom Neo4j health contributors that are aware of the database selection.
68
+
69
+ ==== Imperative variant
70
+
71
+ [[faq.multidatabase.health.imperative]]
72
+ [source,java,indent=0,tabsize=4]
73
+ ----
74
+ import java.util.Optional;
75
+
76
+ import org.neo4j.driver.Driver;
77
+ import org.neo4j.driver.Result;
78
+ import org.neo4j.driver.SessionConfig;
79
+ import org.neo4j.driver.summary.DatabaseInfo;
80
+ import org.neo4j.driver.summary.ResultSummary;
81
+ import org.neo4j.driver.summary.ServerInfo;
82
+ import org.springframework.boot.actuate.health.AbstractHealthIndicator;
83
+ import org.springframework.boot.actuate.health.Health;
84
+ import org.springframework.data.neo4j.core.DatabaseSelection;
85
+ import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
86
+ import org.springframework.util.StringUtils;
87
+
88
+ public class DatabaseSelectionAwareNeo4jHealthIndicator extends AbstractHealthIndicator {
89
+
90
+ private final Driver driver;
91
+
92
+ private final DatabaseSelectionProvider databaseSelectionProvider;
93
+
94
+ public DatabaseSelectionAwareNeo4jHealthIndicator(
95
+ Driver driver, DatabaseSelectionProvider databaseSelectionProvider
96
+ ) {
97
+ this.driver = driver;
98
+ this.databaseSelectionProvider = databaseSelectionProvider;
99
+ }
100
+
101
+ @Override
102
+ protected void doHealthCheck(Health.Builder builder) {
103
+ try {
104
+ SessionConfig sessionConfig = Optional
105
+ .ofNullable(databaseSelectionProvider.getDatabaseSelection())
106
+ .filter(databaseSelection -> databaseSelection != DatabaseSelection.undecided())
107
+ .map(DatabaseSelection::getValue)
108
+ .map(v -> SessionConfig.builder().withDatabase(v).build())
109
+ .orElseGet(SessionConfig::defaultConfig);
110
+
111
+ class Tuple {
112
+ String edition;
113
+ ResultSummary resultSummary;
114
+
115
+ Tuple(String edition, ResultSummary resultSummary) {
116
+ this.edition = edition;
117
+ this.resultSummary = resultSummary;
118
+ }
119
+ }
120
+
121
+ String query =
122
+ "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
123
+ Tuple health = driver.session(sessionConfig)
124
+ .writeTransaction(tx -> {
125
+ Result result = tx.run(query);
126
+ String edition = result.single().get("edition").asString();
127
+ return new Tuple(edition, result.consume());
128
+ });
129
+
130
+ addHealthDetails(builder, health.edition, health.resultSummary);
131
+ } catch (Exception ex) {
132
+ builder.down().withException(ex);
133
+ }
134
+ }
135
+
136
+ static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
137
+ ServerInfo serverInfo = resultSummary.server();
138
+ builder.up()
139
+ .withDetail(
140
+ "server", serverInfo.version() + "@" + serverInfo.address())
141
+ .withDetail("edition", edition);
142
+ DatabaseInfo databaseInfo = resultSummary.database();
143
+ if (StringUtils.hasText(databaseInfo.name())) {
144
+ builder.withDetail("database", databaseInfo.name());
145
+ }
146
+ }
147
+ }
148
+ ----
149
+
150
+ This uses the available database selection to run the same query that Boot runs to check wether a connection is healthy or not.
151
+ Use the following configuration to apply it:
152
+
153
+ [[faq.multidatabase.health.imperative.config]]
154
+ [source,java,indent=0,tabsize=4]
155
+ ----
156
+ import java.util.Map;
157
+
158
+ import org.neo4j.driver.Driver;
159
+ import org.springframework.beans.factory.InitializingBean;
160
+ import org.springframework.boot.actuate.health.CompositeHealthContributor;
161
+ import org.springframework.boot.actuate.health.HealthContributor;
162
+ import org.springframework.boot.actuate.health.HealthContributorRegistry;
163
+ import org.springframework.context.annotation.Bean;
164
+ import org.springframework.context.annotation.Configuration;
165
+ import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
166
+
167
+ @Configuration(proxyBeanMethods = false)
168
+ public class Neo4jHealthConfig {
169
+
170
+ @Bean // <.>
171
+ DatabaseSelectionAwareNeo4jHealthIndicator databaseSelectionAwareNeo4jHealthIndicator(
172
+ Driver driver, DatabaseSelectionProvider databaseSelectionProvider
173
+ ) {
174
+ return new DatabaseSelectionAwareNeo4jHealthIndicator(driver, databaseSelectionProvider);
175
+ }
176
+
177
+ @Bean // <.>
178
+ HealthContributor neo4jHealthIndicator(
179
+ Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators) {
180
+ return CompositeHealthContributor.fromMap(customNeo4jHealthIndicators);
181
+ }
182
+
183
+ @Bean // <.>
184
+ InitializingBean healthContributorRegistryCleaner(
185
+ HealthContributorRegistry healthContributorRegistry,
186
+ Map<String, DatabaseSelectionAwareNeo4jHealthIndicator> customNeo4jHealthIndicators
187
+ ) {
188
+ return () -> customNeo4jHealthIndicators.keySet()
189
+ .stream()
190
+ .map(HealthContributorNameFactory.INSTANCE)
191
+ .forEach(healthContributorRegistry::unregisterContributor);
192
+ }
193
+ }
194
+ ----
195
+ <.> If you have multiple drivers and database selection providers, you would need to create one indicator per combination
196
+ <.> This makes sure that all of those indicators are grouped under Neo4j, replacing the default Neo4j health indicator
197
+ <.> This prevents the individual contributors showing up in the health endpoint directly
198
+
199
+ ==== Reactive variant
200
+
201
+ The reactive variant is basically the same, using reactive types and the corresponding reactive infrastructure classes:
202
+
203
+ [[faq.multidatabase.health.reactive]]
204
+ [source,java,indent=0,tabsize=4]
205
+ ----
206
+ import reactor.core.publisher.Mono;
207
+ import reactor.util.function.Tuple2;
208
+
209
+ import org.neo4j.driver.Driver;
210
+ import org.neo4j.driver.SessionConfig;
211
+ import org.neo4j.driver.reactive.RxResult;
212
+ import org.neo4j.driver.reactive.RxSession;
213
+ import org.neo4j.driver.summary.DatabaseInfo;
214
+ import org.neo4j.driver.summary.ResultSummary;
215
+ import org.neo4j.driver.summary.ServerInfo;
216
+ import org.reactivestreams.Publisher;
217
+ import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
218
+ import org.springframework.boot.actuate.health.Health;
219
+ import org.springframework.data.neo4j.core.DatabaseSelection;
220
+ import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
221
+ import org.springframework.util.StringUtils;
222
+
223
+ public final class DatabaseSelectionAwareNeo4jReactiveHealthIndicator
224
+ extends AbstractReactiveHealthIndicator {
225
+
226
+ private final Driver driver;
227
+
228
+ private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
229
+
230
+ public DatabaseSelectionAwareNeo4jReactiveHealthIndicator(
231
+ Driver driver,
232
+ ReactiveDatabaseSelectionProvider databaseSelectionProvider
233
+ ) {
234
+ this.driver = driver;
235
+ this.databaseSelectionProvider = databaseSelectionProvider;
236
+ }
237
+
238
+ @Override
239
+ protected Mono<Health> doHealthCheck(Health.Builder builder) {
240
+ String query =
241
+ "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
242
+ return databaseSelectionProvider.getDatabaseSelection()
243
+ .map(databaseSelection -> databaseSelection == DatabaseSelection.undecided() ?
244
+ SessionConfig.defaultConfig() :
245
+ SessionConfig.builder().withDatabase(databaseSelection.getValue()).build()
246
+ )
247
+ .flatMap(sessionConfig ->
248
+ Mono.usingWhen(
249
+ Mono.fromSupplier(() -> driver.rxSession(sessionConfig)),
250
+ s -> {
251
+ Publisher<Tuple2<String, ResultSummary>> f = s.readTransaction(tx -> {
252
+ RxResult result = tx.run(query);
253
+ return Mono.from(result.records())
254
+ .map((record) -> record.get("edition").asString())
255
+ .zipWhen((edition) -> Mono.from(result.consume()));
256
+ });
257
+ return Mono.fromDirect(f);
258
+ },
259
+ RxSession::close
260
+ )
261
+ ).map((result) -> {
262
+ addHealthDetails(builder, result.getT1(), result.getT2());
263
+ return builder.build();
264
+ });
265
+ }
266
+
267
+ static void addHealthDetails(Health.Builder builder, String edition, ResultSummary resultSummary) {
268
+ ServerInfo serverInfo = resultSummary.server();
269
+ builder.up()
270
+ .withDetail(
271
+ "server", serverInfo.version() + "@" + serverInfo.address())
272
+ .withDetail("edition", edition);
273
+ DatabaseInfo databaseInfo = resultSummary.database();
274
+ if (StringUtils.hasText(databaseInfo.name())) {
275
+ builder.withDetail("database", databaseInfo.name());
276
+ }
277
+ }
278
+ }
279
+
280
+ ----
281
+
282
+ And of course, the reactive variant of the configuration. It needs two different registry cleaners, as Spring Boot will
283
+ wrap existing reactive indicators to be used with the non-reactive actuator endpoint, too.
284
+
285
+ [[faq.multidatabase.health.reactive.config]]
286
+ [source,java,indent=0,tabsize=4]
287
+ ----
288
+ import java.util.Map;
289
+
290
+ import org.springframework.beans.factory.InitializingBean;
291
+ import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor;
292
+ import org.springframework.boot.actuate.health.HealthContributorNameFactory;
293
+ import org.springframework.boot.actuate.health.HealthContributorRegistry;
294
+ import org.springframework.boot.actuate.health.ReactiveHealthContributor;
295
+ import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry;
296
+ import org.springframework.context.annotation.Bean;
297
+ import org.springframework.context.annotation.Configuration;
298
+
299
+ @Configuration(proxyBeanMethods = false)
300
+ public class Neo4jHealthConfig {
301
+
302
+ @Bean
303
+ ReactiveHealthContributor neo4jHealthIndicator(
304
+ Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
305
+ return CompositeReactiveHealthContributor.fromMap(customNeo4jHealthIndicators);
306
+ }
307
+
308
+ @Bean
309
+ InitializingBean healthContributorRegistryCleaner(HealthContributorRegistry healthContributorRegistry,
310
+ Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
311
+ return () -> customNeo4jHealthIndicators.keySet()
312
+ .stream()
313
+ .map(HealthContributorNameFactory.INSTANCE)
314
+ .forEach(healthContributorRegistry::unregisterContributor);
315
+ }
316
+
317
+ @Bean
318
+ InitializingBean reactiveHealthContributorRegistryCleaner(
319
+ ReactiveHealthContributorRegistry healthContributorRegistry,
320
+ Map<String, DatabaseSelectionAwareNeo4jReactiveHealthIndicator> customNeo4jHealthIndicators) {
321
+ return () -> customNeo4jHealthIndicators.keySet()
322
+ .stream()
323
+ .map(HealthContributorNameFactory.INSTANCE)
324
+ .forEach(healthContributorRegistry::unregisterContributor);
325
+ }
326
+ }
327
+ ----
328
+
329
+
54
330
[[faq.transactions.cluster]]
55
331
== Do I need specific configuration so that transactions work seamless with a Neo4j Causal Cluster?
56
332
0 commit comments