Skip to content

Commit

Permalink
add spectator-web-spring subproject (#318)
Browse files Browse the repository at this point in the history
This module adds a `/spectator/metrics` HTTP endpoint
that returns a different JSON encoding than spring
boot's `/metrics` endpoint. The JSON encoding contains
a richer representation of the registry to enlighten
external observers.

The endpoint supports some filtering mechanisms to
restrict the domain of metrics exposed if the caller
is only interested in particular metrics.
  • Loading branch information
Eric Wiseblatt authored and brharrington committed Sep 27, 2016
1 parent 1906c5e commit 53201cf
Show file tree
Hide file tree
Showing 16 changed files with 2,108 additions and 1 deletion.
3 changes: 2 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ include 'spectator-api',
'spectator-ext-sandbox',
'spectator-ext-spark',
'spectator-reg-metrics3',
'spectator-reg-servo'
'spectator-reg-servo',
'spectator-web-spring'
23 changes: 23 additions & 0 deletions spectator-web-spring/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2016 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

dependencies {
compile project(':spectator-api')

compile 'org.springframework.boot:spring-boot-starter-actuator:1.2.8.RELEASE'
compile 'org.springframework.boot:spring-boot-starter-data-rest:1.2.8.RELEASE'
compile 'com.fasterxml.jackson.core:jackson-databind:2.5.3'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright 2016 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spectator.controllers;

import com.netflix.spectator.api.Gauge;
import com.netflix.spectator.api.Id;
import com.netflix.spectator.api.Measurement;
import com.netflix.spectator.api.Meter;
import com.netflix.spectator.api.Registry;

import com.netflix.spectator.controllers.filter.PrototypeMeasurementFilter;
import com.netflix.spectator.controllers.filter.TagMeasurementFilter;
import com.netflix.spectator.controllers.model.ApplicationRegistry;
import com.netflix.spectator.controllers.model.MetricValues;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.function.Predicate;
import java.util.Map;
import java.util.HashMap;


/**
* Provides an HTTP endpoint for polling spectator metrics.
*/
@RequestMapping("/spectator/metrics")
@RestController
@ConditionalOnExpression("${spectator.webEndpoint.enabled:false}")
public class MetricsController {
@Autowired
private Registry registry;

/**
* A measurement filter that accepts all measurements.
*/
public static final Predicate<Measurement> ALL_MEASUREMENTS_FILTER = m -> true;

@Value("${spectator.applicationName:}")
private String applicationName;

@Value("${spectator.applicationVersion:}")
private String applicationVersion;

@Value("${spectator.webEndpoint.prototypeFilter.path:}")
private String prototypeFilterPath;

private Map<Id, String> knownMeterKinds = new HashMap<Id, String>();
private Predicate<Measurement> defaultMeasurementFilter = null;

/**
* The default measurement filter is configured through properties.
*/
public Predicate<Measurement> getDefaultMeasurementFilter() throws IOException {
if (defaultMeasurementFilter != null) {
return defaultMeasurementFilter;
}

if (!prototypeFilterPath.isEmpty()) {
defaultMeasurementFilter = PrototypeMeasurementFilter.loadFromPath(
prototypeFilterPath);
} else {
defaultMeasurementFilter = ALL_MEASUREMENTS_FILTER;
}

return defaultMeasurementFilter;
}


/**
* Endpoint for querying current metric values.
*
* The result is a JSON document describing the metrics and their values.
* The result can be filtered using query parameters or configuring the
* controller instance itself.
*/
@RequestMapping(method = RequestMethod.GET)
public ApplicationRegistry getMetrics(@RequestParam Map<String, String> filters)
throws IOException {
boolean all = filters.get("all") != null;
String filterMeterNameRegex = filters.getOrDefault("meterNameRegex", "");
String filterTagNameRegex = filters.getOrDefault("tagNameRegex", "");
String filterTagValueRegex = filters.getOrDefault("tagValueRegex", "");
TagMeasurementFilter queryFilter = new TagMeasurementFilter(
filterMeterNameRegex, filterTagNameRegex, filterTagValueRegex);
Predicate<Measurement> filter;
if (all) {
filter = queryFilter;
} else {
filter = queryFilter.and(getDefaultMeasurementFilter());
}

ApplicationRegistry response = new ApplicationRegistry();
response.setApplicationName(applicationName);
response.setApplicationVersion(applicationVersion);
response.setMetrics(encodeRegistry(registry, filter));
return response;
}

/**
* Internal API for encoding a registry that can be encoded as JSON.
* This is a helper function for the REST endpoint and to test against.
*/
Map<String, MetricValues> encodeRegistry(
Registry sourceRegistry, Predicate<Measurement> filter) {
Map<String, MetricValues> metricMap = new HashMap<String, MetricValues>();

/**
* Flatten the meter measurements into a map of measurements keyed by
* the name and mapped to the different tag variants.
*/
for (Meter meter : sourceRegistry) {
String kind = knownMeterKinds.computeIfAbsent(
meter.id(), k -> meterToKind(sourceRegistry, meter));

for (Measurement measurement : meter.measure()) {
if (!filter.test(measurement)) {
continue;
}

String measurementName = measurement.id().name();
MetricValues have = metricMap.get(measurementName);
if (have == null) {
metricMap.put(measurementName, new MetricValues(kind, measurement));
} else {
have.addMeasurement(measurement);
}
}
}
return metricMap;
}

/**
* Determine the type of a meter for reporting purposes.
*
* The accuracy of this method is sensitive to the registry value.
* In practice meters are often AggrMeter or CompositeMeter so it
* is not worth testing the class against the interface because it
* is rarely an actual kind. For lack of a direct way to query
* we will iterate over all the instances in the registry of a given
* type using the registry stream getters.
*
* 20160918(ewiseblatt):
* However, this doesnt work either unless the registry instance
* we have is exactly right and spring seems to get in the way.
* If the registry was a bean, you might need to autowire it.
* But it isnt clear if all the metrics are created through that
* registry. The Spectator.globalRegistry, for example, iterates over
* all the meters using the generic iterator, but the kind-specific
* stream functions are empty if the registry was created as a spring
* bean so you need to passing in an @autowired registry.
*
* @param registry
* Used to provide supplemental information (e.g. to search for the meter).
*
* @param meter
* The meters whose kind we want to know.
*
* @return
* A string such as "Counter". If the type cannot be identified as one of
* the standard Spectator api interface variants, then the simple class name
* is returned.
*/
public static String meterToKind(Registry registry, Meter meter) {
String kind;
if (registry.counters()
.anyMatch(m -> m.id().equals(meter.id()))) {
kind = "Counter";
} else if (registry.timers()
.anyMatch(m -> m.id().equals(meter.id()))) {
kind = "Timer";
} else if (registry.distributionSummaries()
.anyMatch(m -> m.id().equals(meter.id()))) {
kind = "Distribution";
} else if (meter instanceof Gauge) {
kind = "Gauge";
} else {
kind = meter.getClass().getSimpleName();
}
return kind;
}
}
Loading

0 comments on commit 53201cf

Please sign in to comment.