Skip to content

Commit 9852589

Browse files
authored
Add Micrometer Observation support. (#3249)
Signed-off-by: Maryanto <54889592+maryantocinn@users.noreply.github.com>
1 parent 6e86bb2 commit 9852589

14 files changed

+1421
-130
lines changed

pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,13 @@
355355
<scope>test</scope>
356356
</dependency>
357357

358-
</dependencies>
358+
<dependency>
359+
<groupId>io.micrometer</groupId>
360+
<artifactId>micrometer-observation-test</artifactId>
361+
<scope>test</scope>
362+
</dependency>
363+
364+
</dependencies>
359365

360366
<build>
361367
<resources>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.client.elc;
17+
18+
import io.micrometer.common.KeyValues;
19+
20+
/**
21+
* Default {@link ElasticsearchObservationConvention} implementation.
22+
*
23+
* @author maryantocinn
24+
* @since 6.1
25+
*/
26+
public class DefaultElasticsearchObservationConvention implements ElasticsearchObservationConvention {
27+
28+
public static final DefaultElasticsearchObservationConvention INSTANCE = new DefaultElasticsearchObservationConvention();
29+
30+
@Override
31+
public String getName() {
32+
return ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.getName();
33+
}
34+
35+
@Override
36+
public String getContextualName(ElasticsearchObservationContext context) {
37+
38+
String indexName = context.getIndexName();
39+
if (indexName != null) {
40+
return context.getOperationName().getValue() + " " + indexName;
41+
}
42+
return context.getOperationName().getValue();
43+
}
44+
45+
@Override
46+
public KeyValues getLowCardinalityKeyValues(ElasticsearchObservationContext context) {
47+
48+
KeyValues keyValues = KeyValues.of(
49+
ElasticsearchObservation.LowCardinalityKeyNames.OPERATION.withValue(context.getOperationName().getValue()));
50+
51+
String indexName = context.getIndexName();
52+
if (indexName != null) {
53+
keyValues = keyValues.and(ElasticsearchObservation.LowCardinalityKeyNames.COLLECTION.withValue(indexName));
54+
}
55+
56+
return keyValues;
57+
}
58+
59+
@Override
60+
public KeyValues getHighCardinalityKeyValues(ElasticsearchObservationContext context) {
61+
62+
Integer batchSize = context.getBatchSize();
63+
if (batchSize != null) {
64+
return KeyValues.of(
65+
ElasticsearchObservation.HighCardinalityKeyNames.BATCH_SIZE.withValue(String.valueOf(batchSize)));
66+
}
67+
68+
return KeyValues.empty();
69+
}
70+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.client.elc;
17+
18+
import io.micrometer.common.docs.KeyName;
19+
import io.micrometer.observation.Observation;
20+
import io.micrometer.observation.ObservationConvention;
21+
import io.micrometer.observation.docs.ObservationDocumentation;
22+
23+
/**
24+
* {@link ObservationDocumentation} for Spring Data Elasticsearch template operations.
25+
*
26+
* @author maryantocinn
27+
* @since 6.1
28+
*/
29+
public enum ElasticsearchObservation implements ObservationDocumentation {
30+
31+
/**
32+
* Timer created around a Spring Data Elasticsearch template operation.
33+
*/
34+
ELASTICSEARCH_COMMAND_OBSERVATION {
35+
@Override
36+
public String getName() {
37+
return "spring.data.elasticsearch.command";
38+
}
39+
40+
@Override
41+
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
42+
return DefaultElasticsearchObservationConvention.class;
43+
}
44+
45+
@Override
46+
public KeyName[] getLowCardinalityKeyNames() {
47+
return LowCardinalityKeyNames.values();
48+
}
49+
50+
@Override
51+
public KeyName[] getHighCardinalityKeyNames() {
52+
return HighCardinalityKeyNames.values();
53+
}
54+
};
55+
56+
/**
57+
* Low cardinality key names for Spring Data Elasticsearch observations. These become metric dimensions and MUST be
58+
* present on every observation to satisfy backends like Prometheus that require consistent tag key sets.
59+
*/
60+
enum LowCardinalityKeyNames implements KeyName {
61+
62+
/**
63+
* The Spring Data operation being performed (e.g., save, search, delete, bulk).
64+
*/
65+
OPERATION {
66+
@Override
67+
public String asString() {
68+
return "spring.data.operation";
69+
}
70+
},
71+
72+
/**
73+
* The target collection (index) name. Only present when the operation targets a specific index.
74+
*/
75+
COLLECTION {
76+
@Override
77+
public String asString() {
78+
return "spring.data.collection";
79+
}
80+
}
81+
}
82+
83+
/**
84+
* High cardinality key names for Spring Data Elasticsearch observations. These appear only on traces/spans, not on
85+
* metrics, because their values are unbounded or optional per operation.
86+
*/
87+
enum HighCardinalityKeyNames implements KeyName {
88+
89+
/**
90+
* The number of operations included in a batch (bulk) request. Only present for bulk operations.
91+
*/
92+
BATCH_SIZE {
93+
@Override
94+
public String asString() {
95+
return "spring.data.batch.size";
96+
}
97+
}
98+
}
99+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.client.elc;
17+
18+
import org.jspecify.annotations.Nullable;
19+
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
20+
21+
import io.micrometer.observation.Observation;
22+
23+
/**
24+
* {@link Observation.Context} for Spring Data Elasticsearch operations. One instance is created per observed operation.
25+
* It carries contextual data that conventions use to produce observation names and key-values.
26+
*
27+
* @author maryantocinn
28+
* @since 6.1
29+
*/
30+
public class ElasticsearchObservationContext extends Observation.Context {
31+
32+
private final ElasticsearchOperationName operationName;
33+
@Nullable
34+
private final IndexCoordinates indexCoordinates;
35+
@Nullable
36+
private Integer batchSize;
37+
38+
public ElasticsearchObservationContext(ElasticsearchOperationName operationName,
39+
@Nullable IndexCoordinates indexCoordinates) {
40+
this.operationName = operationName;
41+
this.indexCoordinates = indexCoordinates;
42+
}
43+
44+
/**
45+
* @return the Spring Data operation being performed.
46+
*/
47+
public ElasticsearchOperationName getOperationName() {
48+
return operationName;
49+
}
50+
51+
/**
52+
* @return the target index coordinates, or {@literal null} if the operation is not index-specific.
53+
*/
54+
@Nullable
55+
public IndexCoordinates getIndexCoordinates() {
56+
return indexCoordinates;
57+
}
58+
59+
/**
60+
* @return the comma-joined index name(s), or {@literal null} if no index coordinates are set.
61+
*/
62+
@Nullable
63+
public String getIndexName() {
64+
return indexCoordinates != null ? String.join(",", indexCoordinates.getIndexNames()) : null;
65+
}
66+
67+
/**
68+
* @return the batch size, or {@literal null} if not a batch operation.
69+
*/
70+
@Nullable
71+
public Integer getBatchSize() {
72+
return batchSize;
73+
}
74+
75+
/**
76+
* Set the number of operations included in a batch (bulk) request.
77+
*
78+
* @param batchSize the batch size, can be {@literal null}
79+
*/
80+
public void setBatchSize(@Nullable Integer batchSize) {
81+
this.batchSize = batchSize;
82+
}
83+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.client.elc;
17+
18+
import io.micrometer.observation.Observation;
19+
import io.micrometer.observation.ObservationConvention;
20+
21+
/**
22+
* {@link ObservationConvention} for Spring Data Elasticsearch operations. Implement this interface and register it as a
23+
* bean to customize observation names and key-values.
24+
*
25+
* @author maryantocinn
26+
* @since 6.1
27+
*/
28+
public interface ElasticsearchObservationConvention extends ObservationConvention<ElasticsearchObservationContext> {
29+
30+
@Override
31+
default boolean supportsContext(Observation.Context context) {
32+
return context instanceof ElasticsearchObservationContext;
33+
}
34+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.client.elc;
17+
18+
/**
19+
* Enumeration of Spring Data Elasticsearch operation names used in observations.
20+
*
21+
* @author maryantocinn
22+
* @since 6.1
23+
*/
24+
public enum ElasticsearchOperationName {
25+
26+
SAVE("save"), //
27+
INDEX("index"), //
28+
GET("get"), //
29+
MULTI_GET("multiGet"), //
30+
EXISTS("exists"), //
31+
DELETE("delete"), //
32+
DELETE_BY_QUERY("deleteByQuery"), //
33+
BULK("bulk"), //
34+
UPDATE("update"), //
35+
UPDATE_BY_QUERY("updateByQuery"), //
36+
COUNT("count"), //
37+
SEARCH("search");
38+
39+
private final String value;
40+
41+
ElasticsearchOperationName(String value) {
42+
this.value = value;
43+
}
44+
45+
/**
46+
* @return the operation name as a string value used in observation key values.
47+
*/
48+
public String getValue() {
49+
return value;
50+
}
51+
52+
@Override
53+
public String toString() {
54+
return value;
55+
}
56+
}

0 commit comments

Comments
 (0)