Skip to content

Commit ed34567

Browse files
committed
Do a combined metric on multiple properties
For example metrics.properties=pvStatus:Active, Inactive; archive: default, b, !*; archiver: beam, acc, !* Result cf_channel_count{archive="-",archiver="beam",pvStatus="Inactive",} 0.0 cf_channel_count{archive="default",archiver="beam",pvStatus="Inactive",} 0.0 cf_channel_count{archive="b",archiver="acc",pvStatus="Active",} 0.0 cf_channel_count{archive="b",archiver="acc",pvStatus="Inactive",} 0.0 cf_channel_count{archive="-",archiver="beam",pvStatus="Active",} 0.0 cf_channel_count{archive="b",archiver="-",pvStatus="Inactive",} 0.0 cf_channel_count{archive="b",archiver="-",pvStatus="Active",} 0.0 cf_channel_count{archive="-",archiver="-",pvStatus="Active",} 2.0 cf_channel_count{archive="b",archiver="beam",pvStatus="Active",} 0.0 cf_channel_count{archive="default",archiver="acc",pvStatus="Inactive",} 0.0 cf_channel_count{archive="-",archiver="acc",pvStatus="Active",} 0.0 cf_channel_count{archive="b",archiver="beam",pvStatus="Inactive",} 0.0 cf_channel_count{archive="default",archiver="-",pvStatus="Inactive",} 0.0 cf_channel_count{archive="default",archiver="-",pvStatus="Active",} 0.0 cf_channel_count{archive="default",archiver="beam",pvStatus="Active",} 0.0 cf_channel_count{archive="-",archiver="acc",pvStatus="Inactive",} 0.0 cf_channel_count{archive="-",archiver="-",pvStatus="Inactive",} 1.0 cf_channel_count{archive="default",archiver="acc",pvStatus="Active",} 0.0
1 parent 116b227 commit ed34567

File tree

5 files changed

+287
-81
lines changed

5 files changed

+287
-81
lines changed

src/main/java/org/phoebus/channelfinder/ChannelRepository.java

Lines changed: 95 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -465,95 +465,126 @@ private BuiltQuery getBuiltQuery(MultiValueMap<String, String> searchParameters)
465465
}
466466
switch (key) {
467467
case "~name":
468-
for (String value : parameter.getValue()) {
469-
DisMaxQuery.Builder nameQuery = new DisMaxQuery.Builder();
470-
for (String pattern : value.split(valueSplitPattern)) {
471-
nameQuery.queries(getSingleValueQuery("name", pattern.trim()));
472-
}
473-
boolQuery.must(nameQuery.build()._toQuery());
474-
}
468+
addNameQuery(parameter, valueSplitPattern, boolQuery);
475469
break;
476470
case "~tag":
477-
for (String value : parameter.getValue()) {
478-
DisMaxQuery.Builder tagQuery = new DisMaxQuery.Builder();
479-
for (String pattern : value.split(valueSplitPattern)) {
480-
tagQuery.queries(
481-
NestedQuery.of(n -> n.path("tags").query(
482-
getSingleValueQuery("tags.name", pattern.trim())))._toQuery());
483-
}
484-
if (isNot) {
485-
boolQuery.mustNot(tagQuery.build()._toQuery());
486-
} else {
487-
boolQuery.must(tagQuery.build()._toQuery());
488-
}
489-
490-
}
471+
addTagsQuery(parameter, valueSplitPattern, isNot, boolQuery);
491472
break;
492473
case "~size":
493-
Optional<String> maxSize = parameter.getValue().stream().max(Comparator.comparing(Integer::valueOf));
494-
if (maxSize.isPresent()) {
495-
size = Integer.parseInt(maxSize.get());
496-
}
474+
size = parseCountParameter(parameter, size);
497475
break;
498476
case "~from":
499-
Optional<String> maxFrom = parameter.getValue().stream().max(Comparator.comparing(Integer::valueOf));
500-
if (maxFrom.isPresent()) {
501-
from = Integer.parseInt(maxFrom.get());
502-
}
477+
from = parseCountParameter(parameter, from);
503478
break;
504479
case "~search_after":
505480
searchAfter = parameter.getValue().stream().findFirst();
506481
break;
507482

508483
case "~track_total_hits":
509-
Optional<String> firstTrackTotalHits = parameter.getValue().stream().findFirst();
510-
if (firstTrackTotalHits.isPresent()) {
511-
trackTotalHits = Boolean.parseBoolean(firstTrackTotalHits.get());
512-
}
484+
trackTotalHits = isTrackTotalHits(parameter, trackTotalHits);
513485
break;
514486
default:
515-
DisMaxQuery.Builder propertyQuery = new DisMaxQuery.Builder();
516-
for (String value : parameter.getValue()) {
517-
for (String pattern : value.split(valueSplitPattern)) {
518-
String finalKey = key;
519-
BoolQuery bq;
520-
if (isNot) {
521-
bq = BoolQuery.of(p -> p.must(getSingleValueQuery("properties.name", finalKey))
522-
.mustNot(getSingleValueQuery("properties.value", pattern.trim())));
523-
} else {
524-
bq = BoolQuery.of(p -> p.must(getSingleValueQuery("properties.name", finalKey))
525-
.must(getSingleValueQuery("properties.value", pattern.trim())));
526-
}
527-
propertyQuery.queries(
528-
NestedQuery.of(n -> n.path("properties").query(bq._toQuery()))._toQuery()
529-
);
530-
}
531-
}
487+
DisMaxQuery.Builder propertyQuery = calculatePropertiesQuery(parameter, valueSplitPattern, key, isNot);
532488
boolQuery.must(propertyQuery.build()._toQuery());
533489
break;
534490
}
535491
}
536492
return new BuiltQuery(boolQuery, size, from, searchAfter, trackTotalHits);
537493
}
538494

495+
private static DisMaxQuery.Builder calculatePropertiesQuery(Map.Entry<String, List<String>> parameter, String valueSplitPattern, String key, boolean isNot) {
496+
DisMaxQuery.Builder propertyQuery = new DisMaxQuery.Builder();
497+
for (String value : parameter.getValue()) {
498+
for (String pattern : value.split(valueSplitPattern)) {
499+
BoolQuery bq;
500+
bq = calculatePropertyQuery(key, isNot, pattern);
501+
addPropertyQuery(isNot, pattern, propertyQuery, bq);
502+
}
503+
}
504+
return propertyQuery;
505+
}
506+
507+
private static void addPropertyQuery(boolean isNot, String pattern, DisMaxQuery.Builder propertyQuery, BoolQuery bq) {
508+
if (isNot && pattern.trim().equals("*")) {
509+
510+
propertyQuery.queries(
511+
BoolQuery.of( p -> p.mustNot(
512+
NestedQuery.of(n -> n.path("properties").query(bq._toQuery()))._toQuery()
513+
))._toQuery()
514+
);
515+
} else {
516+
517+
propertyQuery.queries(
518+
NestedQuery.of(n -> n.path("properties").query(bq._toQuery()))._toQuery()
519+
);
520+
}
521+
}
522+
523+
private static BoolQuery calculatePropertyQuery(String key, boolean isNot, String pattern) {
524+
BoolQuery bq;
525+
if (isNot) {
526+
if (pattern.trim().equals("*")) {
527+
bq = BoolQuery.of(p -> p.must(getSingleValueQuery("properties.name", key)));
528+
} else {
529+
bq = BoolQuery.of(p -> p.must(getSingleValueQuery("properties.name", key))
530+
.mustNot(getSingleValueQuery("properties.value", pattern.trim())));
531+
}
532+
} else {
533+
bq = BoolQuery.of(p -> p.must(getSingleValueQuery("properties.name", key))
534+
.must(getSingleValueQuery("properties.value", pattern.trim())));
535+
}
536+
return bq;
537+
}
538+
539+
private static boolean isTrackTotalHits(Map.Entry<String, List<String>> parameter, boolean trackTotalHits) {
540+
Optional<String> firstTrackTotalHits = parameter.getValue().stream().findFirst();
541+
if (firstTrackTotalHits.isPresent()) {
542+
trackTotalHits = Boolean.parseBoolean(firstTrackTotalHits.get());
543+
}
544+
return trackTotalHits;
545+
}
546+
547+
private static int parseCountParameter(Map.Entry<String, List<String>> parameter, int size) {
548+
Optional<String> maxSize = parameter.getValue().stream().max(Comparator.comparing(Integer::valueOf));
549+
if (maxSize.isPresent()) {
550+
size = Integer.parseInt(maxSize.get());
551+
}
552+
return size;
553+
}
554+
555+
private static void addTagsQuery(Map.Entry<String, List<String>> parameter, String valueSplitPattern, boolean isNot, BoolQuery.Builder boolQuery) {
556+
for (String value : parameter.getValue()) {
557+
DisMaxQuery.Builder tagQuery = new DisMaxQuery.Builder();
558+
for (String pattern : value.split(valueSplitPattern)) {
559+
tagQuery.queries(
560+
NestedQuery.of(n -> n.path("tags").query(
561+
getSingleValueQuery("tags.name", pattern.trim())))._toQuery());
562+
}
563+
if (isNot) {
564+
boolQuery.mustNot(tagQuery.build()._toQuery());
565+
} else {
566+
boolQuery.must(tagQuery.build()._toQuery());
567+
}
568+
569+
}
570+
}
571+
572+
private static void addNameQuery(Map.Entry<String, List<String>> parameter, String valueSplitPattern, BoolQuery.Builder boolQuery) {
573+
for (String value : parameter.getValue()) {
574+
DisMaxQuery.Builder nameQuery = new DisMaxQuery.Builder();
575+
for (String pattern : value.split(valueSplitPattern)) {
576+
nameQuery.queries(getSingleValueQuery("name", pattern.trim()));
577+
}
578+
boolQuery.must(nameQuery.build()._toQuery());
579+
}
580+
}
581+
539582
private static Query getSingleValueQuery(String name, String pattern) {
540583
return WildcardQuery.of(w -> w.field(name).caseInsensitive(true).value(pattern))._toQuery();
541584
}
542585

543-
private static class BuiltQuery {
544-
public final BoolQuery.Builder boolQuery;
545-
public final Integer size;
546-
public final Integer from;
547-
public final Optional<String> searchAfter;
548-
public final boolean trackTotalHits;
549-
550-
public BuiltQuery(BoolQuery.Builder boolQuery, Integer size, Integer from, Optional<String> searchAfter, boolean trackTotalHits) {
551-
this.boolQuery = boolQuery;
552-
this.size = size;
553-
this.from = from;
554-
this.searchAfter = searchAfter;
555-
this.trackTotalHits = trackTotalHits;
556-
}
586+
private record BuiltQuery(BoolQuery.Builder boolQuery, Integer size, Integer from, Optional<String> searchAfter,
587+
boolean trackTotalHits) {
557588
}
558589

559590
/**

src/main/java/org/phoebus/channelfinder/MetricsService.java

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package org.phoebus.channelfinder;
22

33
import io.micrometer.core.instrument.Gauge;
4+
import io.micrometer.core.instrument.ImmutableTag;
45
import io.micrometer.core.instrument.MeterRegistry;
6+
import io.micrometer.core.instrument.Tag;
57
import org.springframework.beans.factory.annotation.Autowired;
68
import org.springframework.beans.factory.annotation.Value;
79
import org.springframework.context.annotation.PropertySource;
810
import org.springframework.stereotype.Service;
911
import org.springframework.util.LinkedMultiValueMap;
12+
import org.springframework.util.MultiValueMap;
1013

1114
import javax.annotation.PostConstruct;
15+
import java.util.ArrayList;
1216
import java.util.Arrays;
1317
import java.util.List;
1418
import java.util.Map;
1519
import java.util.stream.Collectors;
20+
import java.util.Map.Entry;
1621

1722
@Service
1823
@PropertySource(value = "classpath:application.properties")
@@ -21,12 +26,15 @@ public class MetricsService {
2126
public static final String CF_TOTAL_CHANNEL_COUNT = "cf.total.channel.count";
2227
public static final String CF_PROPERTY_COUNT = "cf.property.count";
2328
public static final String CF_TAG_COUNT = "cf.tag.count";
24-
public static final String CF_PROPERTY_FORMAT_STRING = "cf.%s.channel.count";
29+
public static final String CF_CHANNEL_COUNT = "cf.channel.count";
2530
public static final String CF_TAG_ON_CHANNELS_COUNT = "cf.tag_on_channels.count";
2631
private static final String METRIC_DESCRIPTION_TOTAL_CHANNEL_COUNT = "Count of all ChannelFinder channels";
2732
private static final String METRIC_DESCRIPTION_PROPERTY_COUNT = "Count of all ChannelFinder properties";
2833
private static final String METRIC_DESCRIPTION_TAG_COUNT = "Count of all ChannelFinder tags";
34+
private static final String METRIC_DESCRIPTION_CHANNEL_COUNT = "Count of all ChannelFinder channels with set properties";
2935
private static final String BASE_UNIT = "channels";
36+
private static final String NEGATE = "!";
37+
public static final String NOT_SET = "-";
3038

3139
private final ChannelRepository channelRepository;
3240
private final PropertyRepository propertyRepository;
@@ -90,15 +98,66 @@ private void registerTagMetrics() {
9098
}
9199
}
92100

101+
public static List<MultiValueMap<String, String>> generateAllMultiValueMaps(Map<String, List<String>> properties) {
102+
List<MultiValueMap<String, String>> allMultiValueMaps = new ArrayList<>();
103+
104+
if (properties.isEmpty()) {
105+
allMultiValueMaps.add(new LinkedMultiValueMap<>()); // Add an empty map for the case where all are null
106+
return allMultiValueMaps;
107+
}
108+
109+
List<Entry<String, List<String>>> entries = new ArrayList<>(properties.entrySet());
110+
generateCombinations(entries, 0, new LinkedMultiValueMap<>(), allMultiValueMaps);
111+
112+
return allMultiValueMaps;
113+
}
114+
115+
private static void generateCombinations(
116+
List<Entry<String, List<String>>> entries,
117+
int index,
118+
MultiValueMap<String, String> currentMap,
119+
List<MultiValueMap<String, String>> allMultiValueMaps) {
120+
121+
if (index == entries.size()) {
122+
allMultiValueMaps.add(new LinkedMultiValueMap<>(currentMap));
123+
return;
124+
}
125+
126+
Entry<String, List<String>> currentEntry = entries.get(index);
127+
String key = currentEntry.getKey();
128+
List<String> values = currentEntry.getValue();
129+
130+
// Add the other options
131+
for (String value : values) {
132+
LinkedMultiValueMap<String, String> nextMapWithValue = new LinkedMultiValueMap<>(currentMap);
133+
if (value.startsWith(NEGATE)) {
134+
nextMapWithValue.add(key + NEGATE, value.substring(1));
135+
} else {
136+
nextMapWithValue.add(key, value);
137+
}
138+
generateCombinations(entries, index + 1, nextMapWithValue, allMultiValueMaps);
139+
}
140+
}
141+
142+
private List<Tag> metricTagsFromMultiValueMap(MultiValueMap<String, String> multiValueMap) {
143+
List<Tag> metricTags = new ArrayList<>();
144+
for (Map.Entry<String, String> entry : multiValueMap.toSingleValueMap().entrySet()) {
145+
if (entry.getKey().endsWith(NEGATE)) {
146+
metricTags.add(new ImmutableTag(entry.getKey().substring(0, entry.getKey().length() - 1), NOT_SET));
147+
} else {
148+
metricTags.add(new ImmutableTag(entry.getKey(), entry.getValue()));
149+
}
150+
}
151+
return metricTags;
152+
}
93153
private void registerPropertyMetrics() {
94154
Map<String, List<String>> properties = parseProperties();
95155

96-
properties.forEach((propertyName, propertyValues) -> propertyValues.forEach(propertyValue ->
97-
Gauge.builder(String.format(CF_PROPERTY_FORMAT_STRING, propertyName), () -> channelRepository.countByProperty(propertyName, propertyValue))
98-
.description(String.format("Number of channels with property '%s'", propertyName))
99-
.tag(propertyName, propertyValue)
100-
.baseUnit(BASE_UNIT)
101-
.register(meterRegistry))
156+
List<MultiValueMap<String, String>> combinations = generateAllMultiValueMaps(properties);
157+
combinations.forEach(map -> Gauge.builder(CF_CHANNEL_COUNT, () -> channelRepository.count(map))
158+
.description(METRIC_DESCRIPTION_CHANNEL_COUNT)
159+
.tags(metricTagsFromMultiValueMap(map))
160+
.register(meterRegistry)
102161
);
103162
}
104163
}

src/test/java/org/phoebus/channelfinder/ChannelRepositoryIT.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,34 @@ void findChannels() {
257257
}
258258
}
259259

260+
/**
261+
* find channels using not modifier
262+
*/
263+
@Test
264+
void findLackOfChannels() {
265+
Property extraTestProperty = new Property(testProperties.get(0).getName(), "testOwner", "value2");
266+
Channel testChannel = new Channel("testChannel","testOwner",List.of(testProperties.get(0)),testTags);
267+
Channel testChannel1 = new Channel("testChannel1","testOwner1",testProperties,testTags);
268+
Channel testChannel2 = new Channel("testChannel2","testOwner2",List.of(),List.of());
269+
Channel testChannel3 = new Channel("testChannel3","testOwner3",List.of(extraTestProperty),List.of());
270+
List<Channel> testChannels = Arrays.asList(testChannel, testChannel1, testChannel2, testChannel3);
271+
SearchResult foundChannelsResponse = null;
272+
273+
List<Channel> createdChannels = channelRepository.indexAll(testChannels);
274+
SearchResult createdSearchResult = new SearchResult(createdChannels, 4);
275+
cleanupTestChannels = testChannels;
276+
277+
try {
278+
MultiValueMap<String, String> searchParameters = new LinkedMultiValueMap<>();
279+
searchParameters.set(testProperties.get(0).getName().toLowerCase() + "!", "*");
280+
foundChannelsResponse = channelRepository.search(searchParameters);
281+
Assertions.assertEquals(new SearchResult(List.of(createdChannels.get(2)), 1), foundChannelsResponse);
282+
283+
} catch (ResponseStatusException e) {
284+
Assertions.fail(e);
285+
}
286+
}
287+
260288
/**
261289
* find channels using case insensitive names searches
262290
*/

src/test/java/org/phoebus/channelfinder/MetricsServiceIT.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.springframework.beans.factory.annotation.Autowired;
1313
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
1414
import org.springframework.boot.test.context.SpringBootTest;
15+
import org.springframework.security.core.parameters.P;
1516
import org.springframework.test.context.ActiveProfiles;
1617
import org.springframework.test.context.TestPropertySource;
1718
import org.springframework.test.web.servlet.MockMvc;
@@ -34,7 +35,7 @@
3435
locations = "classpath:application_test.properties",
3536
properties = {
3637
"metrics.tags=testTag0, testTag1",
37-
"metrics.properties=testProperty0: value0, value1; testProperty1: value0"
38+
"metrics.properties=testProperty0: value0, value1; testProperty1: value0, !*"
3839
})
3940
class MetricsServiceIT {
4041

@@ -155,11 +156,7 @@ private String propertyParamValue(Property property) {
155156
}
156157

157158
private void getAndExpectPropertyMetric(Property property, int expectedValue) throws Exception {
158-
getAndExpectMetric(propertyParamValue(property), String.format(MetricsService.CF_PROPERTY_FORMAT_STRING, property.getName()), expectedValue);
159-
}
160-
161-
private void getAndExpectParentPropertyMetric(Property property, int expectedValue) throws Exception {
162-
getAndExpectMetricParent(String.format(MetricsService.CF_PROPERTY_FORMAT_STRING, property.getName()), expectedValue);
159+
getAndExpectMetric(propertyParamValue(property), MetricsService.CF_CHANNEL_COUNT, expectedValue);
163160
}
164161

165162
@Test
@@ -180,24 +177,24 @@ void testPropertyMultiGaugeMetrics() throws Exception {
180177
List.of(new Property(testProperties.get(0).getName(), OWNER, PROPERTY_VALUE + 1)),
181178
List.of());
182179

183-
getAndExpectParentPropertyMetric(testProperties.get(0), 0);
180+
getAndExpectMetricParent(MetricsService.CF_CHANNEL_COUNT, 0);
184181
getAndExpectPropertyMetric(testChannel.getProperties().get(0), 0);
185182
getAndExpectPropertyMetric(testChannel1.getProperties().get(0), 0);
186183

187-
getAndExpectParentPropertyMetric(testProperties.get(1), 0);
188184
getAndExpectPropertyMetric(testChannel.getProperties().get(1), 0);
185+
getAndExpectPropertyMetric(new Property(testProperties.get(1).getName(), OWNER, MetricsService.NOT_SET), 0);
189186

190187
propertyRepository.saveAll(testProperties);
191188

192189
channelRepository.save(testChannel);
193190
channelRepository.save(testChannel1);
194191

195-
getAndExpectParentPropertyMetric(testProperties.get(0), 2);
192+
getAndExpectMetricParent(MetricsService.CF_CHANNEL_COUNT, 2);
196193
getAndExpectPropertyMetric(testChannel.getProperties().get(0), 1);
197194
getAndExpectPropertyMetric(testChannel1.getProperties().get(0), 1);
198195

199-
getAndExpectParentPropertyMetric(testProperties.get(1), 1);
200196
getAndExpectPropertyMetric(testChannel.getProperties().get(1), 1);
197+
getAndExpectPropertyMetric(new Property(testProperties.get(1).getName(), OWNER, MetricsService.NOT_SET), 1);
201198

202199
}
203200
}

0 commit comments

Comments
 (0)