Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 51 additions & 43 deletions src/main/java/org/phoebus/channelfinder/MetricsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,92 +11,100 @@
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

@Service
@PropertySource(value = "classpath:application.properties")
public class MetricsService {

private static final Logger logger = Logger.getLogger(MetricsService.class.getName());
public static final String CF_TOTAL_CHANNEL_COUNT = "cf.total.channel.count";
public static final String CF_PROPERTY_COUNT = "cf.property.count";
public static final String CF_TAG_COUNT = "cf.tag.count";
public static final String CF_PROPERTY_FORMAT_STRING = "cf.%s.channel.count";
public static final String CF_CHANNEL_COUNT = "cf.channel.count";
private static final String METRIC_DESCRIPTION_TOTAL_CHANNEL_COUNT = "Count of all ChannelFinder channels";
private static final String METRIC_DESCRIPTION_PROPERTY_COUNT = "Count of all ChannelFinder properties";
private static final String METRIC_DESCRIPTION_TAG_COUNT = "Count of all ChannelFinder tags";
private static final String METRIC_DESCRIPTION_CHANNEL_COUNT =
"Count of channels with specific property with and specific value";

private final ChannelRepository channelRepository;
private final PropertyRepository propertyRepository;
private final TagRepository tagRepository;
private final MeterRegistry meterRegistry;

MultiGauge channelCounts;

@Value("${metrics.tags}")
private String[] tags;

@Value("#{${metrics.properties:{{'pvStatus', 'Active'}, {'pvStatus', 'Inactive'}}}}")
private String[][] properties;
@Value("${metrics.properties}")
private String metricProperties;

Map<String, List<String>> parseProperties() {
if (metricProperties == null || metricProperties.isEmpty()) {
return new LinkedMultiValueMap<>();
}
return Arrays.stream(metricProperties.split(";")).map(s ->
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to give an explicit example of using this syntax; the application.properties file only has a single metricProperty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some docs.

{
String[] split = s.split(":");
String k = split[0].trim();
List<String> v = Arrays.stream(split[1].split(",")).map(String::trim).toList();
return Map.entry(k, v);
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

@Autowired
public MetricsService(
final ChannelRepository channelRepository,
final PropertyRepository propertyRepository,
final TagRepository tagRepository,
final MeterRegistry meterRegistry) {
final ChannelRepository channelRepository,
final PropertyRepository propertyRepository,
final TagRepository tagRepository,
final MeterRegistry meterRegistry) {
this.channelRepository = channelRepository;
this.propertyRepository = propertyRepository;
this.tagRepository = tagRepository;
this.meterRegistry = meterRegistry;
registerGaugeMetrics();
}

@PostConstruct
private void registerGaugeMetrics() {
Gauge.builder(CF_TOTAL_CHANNEL_COUNT, () -> channelRepository.count(new LinkedMultiValueMap<>()))
.description(METRIC_DESCRIPTION_TOTAL_CHANNEL_COUNT)
.register(meterRegistry);
.description(METRIC_DESCRIPTION_TOTAL_CHANNEL_COUNT)
.register(meterRegistry);
Gauge.builder(CF_PROPERTY_COUNT, propertyRepository::count)
.description(METRIC_DESCRIPTION_PROPERTY_COUNT)
.register(meterRegistry);
.description(METRIC_DESCRIPTION_PROPERTY_COUNT)
.register(meterRegistry);
Gauge.builder(CF_TAG_COUNT, tagRepository::count)
.description(METRIC_DESCRIPTION_TAG_COUNT)
.register(meterRegistry);
channelCounts = MultiGauge.builder(CF_CHANNEL_COUNT)
.description(METRIC_DESCRIPTION_CHANNEL_COUNT)
.baseUnit("channels")
.register(meterRegistry);
.description(METRIC_DESCRIPTION_TAG_COUNT)
.register(meterRegistry);
registerTagMetrics();
registerPropertyMetrics();
}

@Scheduled(fixedRate = 5000)
public void updateMetrics() {
logger.log(
Level.FINER,
() -> "Updating metrics for properties " + Arrays.deepToString(properties) + " and tags " + Arrays.toString(tags));
ArrayList<MultiGauge.Row<?>> rows = new ArrayList<>();
private void registerTagMetrics() {

// Add tags
for (String tag: tags) {
long count = channelRepository.countByTag(tag);
rows.add(MultiGauge.Row.of(Tags.of("tag", tag), count ));
logger.log(
Level.FINER,
() -> "Updating metrics for tag " + tag + " to " + count);
for (String tag : tags) {
Gauge.builder(CF_CHANNEL_COUNT, () -> channelRepository.countByTag(tag))
.description("Number of channels with tag")
.tag("tag", tag)
.baseUnit("channels")
.register(meterRegistry);
}
}

// Add properties
for (String[] propertyValue: properties) {
long count = channelRepository.countByProperty(propertyValue[0], propertyValue[1]);
rows.add(MultiGauge.Row.of(Tags.of(propertyValue[0], propertyValue[1]), count));
logger.log(
Level.FINER,
() -> "Updating metrics for property " + propertyValue[0] + ":" + propertyValue[1] + " to " + count);
}
private void registerPropertyMetrics() {
Map<String, List<String>> properties = parseProperties();

channelCounts.register(rows, true);
properties.forEach((propertyName, propertyValues) -> propertyValues.forEach(propertyValue ->
Gauge.builder(String.format(CF_PROPERTY_FORMAT_STRING, propertyName), () -> channelRepository.countByProperty(propertyName, propertyValue))
.description(String.format("Number of channels with property '%s'", propertyName))
.tag(propertyName, propertyValue)
.baseUnit("channels")
.register(meterRegistry))
);
}
}
2 changes: 1 addition & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,4 @@ aa.auto_pause=
#actuator
management.endpoints.web.exposure.include=prometheus, metrics, health, info
metrics.tags=
metrics.properties={{'pvStatus', 'Active'}, {'pvStatus', 'Inactive'}}
metrics.properties=pvStatus:Active, Inactive
25 changes: 25 additions & 0 deletions src/site/sphinx/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,28 @@ Or to not have the EPICS PV Access Server listen, then:

EPICS_PVAS_INTF_ADDR_LIST="0.0.0.0"

Metrics
^^^^^^^

Metrics can be exposed by setting the `management.endpoints.web.exposure.include=prometheus` property.

.. code-block::

management.endpoints.web.exposure.include=prometheus, metrics, health, info

Adding the prometheus property will expose the prometheus endpoint which can be scraped by prometheus.

You can also set the metrics.tags to add counts of number of channels per tag. These are exposed as
`cf_channel_count{tag=tagName}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since channels can have any number of tags, doesn't this break the sum channel count?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the name to try and make it more clear what it means. And correspondingly updated the documentation.


.. code-block::

metrics.tags=Accelerator, Beamline, Beamline1, Beamline2, Beamline3

You can also set the metrics.properties to add counts of number of channels per property and value. These are exposed as
`cf_propertyName_channels_count{propertyName=propertyValue}`.


.. code-block::

metrics.properties=pvStatus:Active, Inactive; archive: default, fast, slow; archiver: aa_beamline, aa_acccelerator
23 changes: 12 additions & 11 deletions src/test/java/org/phoebus/channelfinder/MetricsServiceIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,23 @@
locations = "classpath:application_test.properties",
properties = {
"metrics.tags=testTag0, testTag1",
"metrics.properties={{'testProperty0', 'testProperty0Value'}, {'testProperty1', 'testProperty1Value'}}"
"metrics.properties=testProperty0: testProperty0Value; testProperty1: testProperty1Value"
})
class MetricsServiceIT {

public static final String METRICS_ENDPOINT = "/actuator/metrics/";
public static final String METRICS_TAG_LABEL = "tag";
public static final String PROPERTY_0_LABEL = "testProperty0:testProperty0Value";
public static final String PROPERTY_1_LABEL = "testProperty1:testProperty1Value";
public static final String METRICS_PROPERTY_NAME = "testProperty";
public static final String PROPERTY_0_LABEL = METRICS_PROPERTY_NAME + "0:testProperty0Value";
public static final String PROPERTY_1_LABEL = METRICS_PROPERTY_NAME + "1:testProperty1Value";
public static final String TAG_0_LABEL = "tag:testTag0";
public static final String TAG_1_LABEL = "tag:testTag1";
private final List<Tag> testTags =
Arrays.asList(new Tag("testTag0", "testTagOwner0"), new Tag("testTag1", "testTagOwner1"));
private final List<Property> testProperties = Arrays.asList(
new Property("testProperty0", "testPropertyOwner0"),
new Property("testProperty1", "testPropertyOwner1"),
new Property("testProperty2", "testPropertyOwner2"));
new Property(METRICS_PROPERTY_NAME + "0", "testPropertyOwner0"),
new Property(METRICS_PROPERTY_NAME + "1", "testPropertyOwner1"),
new Property(METRICS_PROPERTY_NAME + "2", "testPropertyOwner2"));

@Autowired
ChannelRepository channelRepository;
Expand Down Expand Up @@ -141,10 +142,10 @@ void testTagMultiGaugeMetrics() throws Exception {

@Test
void testPropertyMultiGaugeMetrics() throws Exception {
mockMvc.perform(get(METRICS_ENDPOINT + MetricsService.CF_CHANNEL_COUNT)
mockMvc.perform(get(METRICS_ENDPOINT + String.format(MetricsService.CF_PROPERTY_FORMAT_STRING, METRICS_PROPERTY_NAME + "0"))
.param(METRICS_TAG_LABEL, PROPERTY_0_LABEL))
.andExpect(jsonPath("$.measurements[0].value").value(0));
mockMvc.perform(get(METRICS_ENDPOINT + MetricsService.CF_CHANNEL_COUNT)
mockMvc.perform(get(METRICS_ENDPOINT + String.format(MetricsService.CF_PROPERTY_FORMAT_STRING, METRICS_PROPERTY_NAME + "1"))
.param(METRICS_TAG_LABEL, PROPERTY_1_LABEL))
.andExpect(jsonPath("$.measurements[0].value").value(0));

Expand All @@ -156,15 +157,15 @@ void testPropertyMultiGaugeMetrics() throws Exception {
"testOwner",
testProperties.stream()
.map(p -> new Property(p.getName(), p.getOwner(), p.getName() + "Value"))
.collect(Collectors.toList()),
.toList(),
testTags);
channelRepository.save(testChannel);

await().untilAsserted(() -> {
mockMvc.perform(get(METRICS_ENDPOINT + MetricsService.CF_CHANNEL_COUNT)
mockMvc.perform(get(METRICS_ENDPOINT + String.format(MetricsService.CF_PROPERTY_FORMAT_STRING, METRICS_PROPERTY_NAME + "0"))
.param(METRICS_TAG_LABEL, PROPERTY_0_LABEL))
.andExpect(jsonPath("$.measurements[0].value").value(1));
mockMvc.perform(get(METRICS_ENDPOINT + MetricsService.CF_CHANNEL_COUNT)
mockMvc.perform(get(METRICS_ENDPOINT + String.format(MetricsService.CF_PROPERTY_FORMAT_STRING, METRICS_PROPERTY_NAME + "1"))
.param(METRICS_TAG_LABEL, PROPERTY_1_LABEL))
.andExpect(jsonPath("$.measurements[0].value").value(1));
});
Expand Down
2 changes: 1 addition & 1 deletion src/test/resources/application_test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,4 @@ aa.auto_pause=pvStatus,archive
#actuator
management.endpoints.web.exposure.include=prometheus, metrics, health, info
metrics.tags=group4_10
metrics.properties={{'group4', '10'}, {'group5', '10'}}
metrics.properties=group4: 10; group5: 10
Loading