Skip to content

Commit 3a6745e

Browse files
committedAug 23, 2022
#976 switch metrics capturing to promethian style and basic promethian metrics endpoint
1 parent d6b0a44 commit 3a6745e

File tree

18 files changed

+321
-122
lines changed

18 files changed

+321
-122
lines changed
 

‎jekyll-www.mock-server.com/mock_server/_includes/logging_configuration.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ <h2>Logging & Metrics Configuration:</h2>
8585

8686
<button id="button_configuration_enabled_metrics" class="accordion title"><strong>Enable Metrics</strong></button>
8787
<div class="panel title">
88-
<p>Enable the recording of metrics for different activities within MockServer, such as, EXPECTATION_NOT_MATCHED_COUNT, ACTION_RESPONSE_COUNT, WEBSOCKET_CALLBACK_CLIENT_COUNT, etc</p>
88+
<p>Enable the recording of metrics for different activities within MockServer, these are exposed via /mockserver/metrics in prometheus format</p>
8989
<p>Type: <span class="keyword">boolean</span> Default: <span class="this_value">false</span></p>
9090
<p>Java Code:</p>
9191
<pre class="prettyprint lang-java code"><code class="code">ConfigurationProperties.metricsEnabled(boolean enable)</code></pre>

‎mockserver-client-java/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@
207207
<pattern>io.swagger</pattern>
208208
<shadedPattern>shaded_package.io.swagger</shadedPattern>
209209
</relocation>
210+
<relocation>
211+
<pattern>io.prometheus</pattern>
212+
<shadedPattern>shaded_package.io.prometheus</shadedPattern>
213+
</relocation>
210214
<relocation>
211215
<pattern>jakarta.activation</pattern>
212216
<shadedPattern>shaded_package.jakarta.activation</shadedPattern>

‎mockserver-core/pom.xml

+10
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,16 @@
231231
<optional>true</optional> <!-- do not impose on project importing mock-server -->
232232
</dependency>
233233

234+
<!-- prometheus -->
235+
<dependency>
236+
<groupId>io.prometheus</groupId>
237+
<artifactId>simpleclient</artifactId>
238+
</dependency>
239+
<dependency>
240+
<groupId>io.prometheus</groupId>
241+
<artifactId>simpleclient_httpserver</artifactId>
242+
</dependency>
243+
234244
<!-- test -->
235245
<dependency>
236246
<groupId>junit</groupId>

‎mockserver-core/src/main/java/org/mockserver/closurecallback/websocketregistry/WebSocketClientRegistry.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public void registerClient(String clientId, ChannelHandlerContext ctx) {
110110
throw new WebSocketException("Exception while sending web socket registration client id message to client " + clientId, e);
111111
}
112112
clientRegistry.put(clientId, ctx.channel());
113-
metrics.set(WEBSOCKET_CALLBACK_CLIENT_COUNT, clientRegistry.size());
113+
metrics.set(WEBSOCKET_CALLBACK_CLIENTS_COUNT, clientRegistry.size());
114114
if (MockServerLogger.isEnabled(TRACE) && mockServerLogger != null) {
115115
mockServerLogger.logEvent(
116116
new LogEntry()
@@ -126,7 +126,7 @@ public void unregisterClient(String clientId) {
126126
if (removeChannel != null && removeChannel.isOpen()) {
127127
removeChannel.close();
128128
}
129-
metrics.set(WEBSOCKET_CALLBACK_CLIENT_COUNT, clientRegistry.size());
129+
metrics.set(WEBSOCKET_CALLBACK_CLIENTS_COUNT, clientRegistry.size());
130130
if (MockServerLogger.isEnabled(TRACE) && mockServerLogger != null) {
131131
mockServerLogger.logEvent(
132132
new LogEntry()
@@ -138,7 +138,7 @@ public void unregisterClient(String clientId) {
138138

139139
public void registerResponseCallbackHandler(String webSocketCorrelationId, WebSocketResponseCallback expectationResponseCallback) {
140140
responseCallbackRegistry.put(webSocketCorrelationId, expectationResponseCallback);
141-
metrics.set(WEBSOCKET_CALLBACK_RESPONSE_HANDLER_COUNT, responseCallbackRegistry.size());
141+
metrics.set(WEBSOCKET_CALLBACK_RESPONSE_HANDLERS_COUNT, responseCallbackRegistry.size());
142142
if (MockServerLogger.isEnabled(TRACE) && mockServerLogger != null) {
143143
mockServerLogger.logEvent(
144144
new LogEntry()
@@ -150,7 +150,7 @@ public void registerResponseCallbackHandler(String webSocketCorrelationId, WebSo
150150

151151
public void unregisterResponseCallbackHandler(String webSocketCorrelationId) {
152152
responseCallbackRegistry.remove(webSocketCorrelationId);
153-
metrics.set(WEBSOCKET_CALLBACK_RESPONSE_HANDLER_COUNT, responseCallbackRegistry.size());
153+
metrics.set(WEBSOCKET_CALLBACK_RESPONSE_HANDLERS_COUNT, responseCallbackRegistry.size());
154154
if (MockServerLogger.isEnabled(TRACE) && mockServerLogger != null) {
155155
mockServerLogger.logEvent(
156156
new LogEntry()
@@ -162,7 +162,7 @@ public void unregisterResponseCallbackHandler(String webSocketCorrelationId) {
162162

163163
public void registerForwardCallbackHandler(String webSocketCorrelationId, WebSocketRequestCallback expectationForwardCallback) {
164164
forwardCallbackRegistry.put(webSocketCorrelationId, expectationForwardCallback);
165-
metrics.set(WEBSOCKET_CALLBACK_FORWARD_HANDLER_COUNT, forwardCallbackRegistry.size());
165+
metrics.set(WEBSOCKET_CALLBACK_FORWARD_HANDLERS_COUNT, forwardCallbackRegistry.size());
166166
if (MockServerLogger.isEnabled(TRACE) && mockServerLogger != null) {
167167
mockServerLogger.logEvent(
168168
new LogEntry()
@@ -174,7 +174,7 @@ public void registerForwardCallbackHandler(String webSocketCorrelationId, WebSoc
174174

175175
public void unregisterForwardCallbackHandler(String webSocketCorrelationId) {
176176
forwardCallbackRegistry.remove(webSocketCorrelationId);
177-
metrics.set(WEBSOCKET_CALLBACK_FORWARD_HANDLER_COUNT, forwardCallbackRegistry.size());
177+
metrics.set(WEBSOCKET_CALLBACK_FORWARD_HANDLERS_COUNT, forwardCallbackRegistry.size());
178178
if (MockServerLogger.isEnabled(TRACE) && mockServerLogger != null) {
179179
mockServerLogger.logEvent(
180180
new LogEntry()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.mockserver.metrics;
2+
3+
import io.prometheus.client.Collector;
4+
import io.prometheus.client.GaugeMetricFamily;
5+
import org.mockserver.version.Version;
6+
7+
import java.util.Collections;
8+
import java.util.List;
9+
import java.util.concurrent.atomic.AtomicReference;
10+
11+
import static java.util.Arrays.asList;
12+
13+
/**
14+
* @author jamesdbloom
15+
*/
16+
public class BuildInfoCollector extends Collector {
17+
18+
public List<Collector.MetricFamilySamples> collect() {
19+
String version = Version.getVersion();
20+
String majorMinorVersion = Version.getMajorMinorVersion();
21+
String groupId = Version.getGroupId();
22+
String artifactId = Version.getArtifactId();
23+
24+
return Collections.singletonList(
25+
new GaugeMetricFamily(
26+
"mock_server_build_info",
27+
"Mock Server build information",
28+
asList(
29+
"version",
30+
"major_minor_version",
31+
"group_id",
32+
"artifact_id"
33+
)
34+
).addMetric(asList(
35+
version != null ? version : "unknown",
36+
majorMinorVersion != null ? majorMinorVersion : "unknown",
37+
groupId != null ? groupId : "unknown",
38+
artifactId != null ? artifactId : "unknown"
39+
), 1L)
40+
);
41+
}
42+
43+
}
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,151 @@
11
package org.mockserver.metrics;
22

3+
import io.prometheus.client.Gauge;
34
import org.mockserver.configuration.Configuration;
5+
import org.mockserver.log.model.LogEntry;
6+
import org.mockserver.logging.MockServerLogger;
47
import org.mockserver.model.Action;
58

9+
import java.util.Arrays;
610
import java.util.Map;
711
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.concurrent.atomic.AtomicReference;
13+
14+
import static org.mockserver.log.model.LogEntry.LogMessageType.EXCEPTION;
815

916
/**
1017
* @author jamesdbloom
1118
*/
1219
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "FieldMayBeFinal"})
1320
public class Metrics {
1421

15-
private static Map<Name, Integer> metrics = new ConcurrentHashMap<>();
16-
private final Configuration configuration;
22+
private static final AtomicReference<Boolean> additionalMetricsRegistered = new AtomicReference<>(false);
23+
private static final Map<Name, Gauge> metrics = new ConcurrentHashMap<>();
24+
25+
private final Boolean metricsEnabled;
1726

1827
public Metrics(Configuration configuration) {
19-
this.configuration = configuration;
28+
metricsEnabled = configuration.metricsEnabled();
29+
if (metricsEnabled && additionalMetricsRegistered.compareAndSet(false, true)) {
30+
new BuildInfoCollector().register();
31+
Arrays.stream(Name.values()).forEach(Metrics::getOrCreate);
32+
}
33+
}
34+
35+
private static Gauge getOrCreate(Name name) {
36+
synchronized (name) {
37+
Gauge gauge = metrics.get(name);
38+
if (gauge == null) {
39+
try {
40+
gauge = Gauge.build()
41+
.name(name.name().toLowerCase())
42+
.help(name.description)
43+
.register();
44+
metrics.put(name, gauge);
45+
} catch (Throwable throwable) {
46+
new MockServerLogger().logEvent(
47+
new LogEntry()
48+
.setType(EXCEPTION)
49+
.setMessageFormat("exception:{} creating metric:{}")
50+
.setArguments(throwable.getMessage(), name.name())
51+
.setThrowable(throwable)
52+
);
53+
}
54+
}
55+
return gauge;
56+
}
2057
}
2158

2259
public static void clear() {
23-
metrics.clear();
60+
metrics.forEach((name, gauge) -> gauge.set(0));
61+
}
62+
63+
public static void clear(Name name) {
64+
getOrCreate(name).set(0);
2465
}
2566

2667
public void set(Name name, Integer value) {
27-
metrics.put(name, value);
68+
if (metricsEnabled) {
69+
getOrCreate(name).set(value);
70+
}
2871
}
2972

3073
public static Integer get(Name name) {
31-
Integer value = metrics.get(name);
32-
return value != null ? value : 0;
74+
return (int) getOrCreate(name).get();
3375
}
3476

3577
public void increment(Name name) {
36-
if (configuration.metricsEnabled()) {
37-
synchronized (name) {
38-
metrics.merge(name, 1, Integer::sum);
39-
}
78+
if (metricsEnabled) {
79+
getOrCreate(name).inc();
4080
}
4181
}
4282

43-
public void decrement(Name name) {
44-
if (configuration.metricsEnabled()) {
45-
synchronized (name) {
46-
final Integer currentValue = metrics.get(name);
47-
if (currentValue != null) {
48-
metrics.put(name, currentValue - 1);
49-
} else {
50-
throw new IllegalArgumentException("Can not decrement metric \"" + name + "\" because it not exist");
51-
}
52-
}
83+
public void increment(Action.Type type) {
84+
if (metricsEnabled) {
85+
increment(Name.valueOf(type.name() + "_ACTIONS_COUNT"));
5386
}
5487
}
5588

56-
public void increment(Action.Type type) {
57-
if (configuration.metricsEnabled()) {
58-
Name name = Name.valueOf("ACTION_" + type.name() + "_COUNT");
59-
synchronized (name) {
60-
metrics.merge(name, 1, Integer::sum);
61-
}
89+
public void decrement(Name name) {
90+
if (metricsEnabled) {
91+
getOrCreate(name).dec();
6292
}
6393
}
6494

6595
public void decrement(Action.Type type) {
66-
if (configuration.metricsEnabled()) {
67-
Name name = Name.valueOf("ACTION_" + type.name() + "_COUNT");
68-
synchronized (name) {
69-
final Integer currentValue = metrics.get(name);
70-
if (currentValue != null) {
71-
metrics.put(name, currentValue - 1);
72-
} else {
73-
throw new IllegalArgumentException("Can not decrement metric \"" + name + "\" because it not exist");
74-
}
75-
}
96+
if (metricsEnabled) {
97+
decrement(Name.valueOf(type.name() + "_ACTIONS_COUNT"));
7698
}
7799
}
78100

101+
public static void clearRequestAndExpectationMetrics() {
102+
clear(Name.REQUESTS_RECEIVED_COUNT);
103+
clear(Name.EXPECTATIONS_NOT_MATCHED_COUNT);
104+
clear(Name.RESPONSE_EXPECTATIONS_MATCHED_COUNT);
105+
}
106+
79107
public static void clearActionMetrics() {
80-
metrics.remove(Name.ACTION_FORWARD_COUNT);
81-
metrics.remove(Name.ACTION_FORWARD_TEMPLATE_COUNT);
82-
metrics.remove(Name.ACTION_FORWARD_CLASS_CALLBACK_COUNT);
83-
metrics.remove(Name.ACTION_FORWARD_OBJECT_CALLBACK_COUNT);
84-
metrics.remove(Name.ACTION_FORWARD_REPLACE_COUNT);
85-
metrics.remove(Name.ACTION_RESPONSE_COUNT);
86-
metrics.remove(Name.ACTION_RESPONSE_TEMPLATE_COUNT);
87-
metrics.remove(Name.ACTION_RESPONSE_CLASS_CALLBACK_COUNT);
88-
metrics.remove(Name.ACTION_RESPONSE_OBJECT_CALLBACK_COUNT);
89-
metrics.remove(Name.ACTION_ERROR_COUNT);
108+
clear(Name.FORWARD_ACTIONS_COUNT);
109+
clear(Name.FORWARD_TEMPLATE_ACTIONS_COUNT);
110+
clear(Name.FORWARD_CLASS_CALLBACK_ACTIONS_COUNT);
111+
clear(Name.FORWARD_OBJECT_CALLBACK_ACTIONS_COUNT);
112+
clear(Name.FORWARD_REPLACE_ACTIONS_COUNT);
113+
clear(Name.RESPONSE_ACTIONS_COUNT);
114+
clear(Name.RESPONSE_TEMPLATE_ACTIONS_COUNT);
115+
clear(Name.RESPONSE_CLASS_CALLBACK_ACTIONS_COUNT);
116+
clear(Name.RESPONSE_OBJECT_CALLBACK_ACTIONS_COUNT);
117+
clear(Name.ERROR_ACTIONS_COUNT);
90118
}
91119

92120
public static void clearWebSocketMetrics() {
93-
metrics.remove(Name.WEBSOCKET_CALLBACK_CLIENT_COUNT);
94-
metrics.remove(Name.WEBSOCKET_CALLBACK_RESPONSE_HANDLER_COUNT);
95-
metrics.remove(Name.WEBSOCKET_CALLBACK_FORWARD_HANDLER_COUNT);
121+
clear(Name.WEBSOCKET_CALLBACK_CLIENTS_COUNT);
122+
clear(Name.WEBSOCKET_CALLBACK_RESPONSE_HANDLERS_COUNT);
123+
clear(Name.WEBSOCKET_CALLBACK_FORWARD_HANDLERS_COUNT);
96124
}
97125

98126
public enum Name {
99-
EXPECTATION_NOT_MATCHED_COUNT,
100-
RESPONSE_EXPECTATION_MATCHED_COUNT,
101-
FORWARD_EXPECTATION_MATCHED_COUNT,
102-
ACTION_FORWARD_COUNT,
103-
ACTION_FORWARD_TEMPLATE_COUNT,
104-
ACTION_FORWARD_CLASS_CALLBACK_COUNT,
105-
ACTION_FORWARD_OBJECT_CALLBACK_COUNT,
106-
ACTION_FORWARD_REPLACE_COUNT,
107-
ACTION_RESPONSE_COUNT,
108-
ACTION_RESPONSE_TEMPLATE_COUNT,
109-
ACTION_RESPONSE_CLASS_CALLBACK_COUNT,
110-
ACTION_RESPONSE_OBJECT_CALLBACK_COUNT,
111-
ACTION_ERROR_COUNT,
112-
WEBSOCKET_CALLBACK_CLIENT_COUNT,
113-
WEBSOCKET_CALLBACK_RESPONSE_HANDLER_COUNT,
114-
WEBSOCKET_CALLBACK_FORWARD_HANDLER_COUNT
127+
REQUESTS_RECEIVED_COUNT("Expectation not matched count"),
128+
EXPECTATIONS_NOT_MATCHED_COUNT("Expectation not matched count"),
129+
RESPONSE_EXPECTATIONS_MATCHED_COUNT("Response expectation matched count"),
130+
FORWARD_EXPECTATIONS_MATCHED_COUNT("Forward expectation matched count"),
131+
FORWARD_ACTIONS_COUNT("Action forward count"),
132+
FORWARD_TEMPLATE_ACTIONS_COUNT("Action forward template count"),
133+
FORWARD_CLASS_CALLBACK_ACTIONS_COUNT("Action forward class callback count"),
134+
FORWARD_OBJECT_CALLBACK_ACTIONS_COUNT("Action forward object callback count"),
135+
FORWARD_REPLACE_ACTIONS_COUNT("Action forward replace count"),
136+
RESPONSE_ACTIONS_COUNT("Action response count"),
137+
RESPONSE_TEMPLATE_ACTIONS_COUNT("Action response template count"),
138+
RESPONSE_CLASS_CALLBACK_ACTIONS_COUNT("Action response class callback count"),
139+
RESPONSE_OBJECT_CALLBACK_ACTIONS_COUNT("Action response object callback count"),
140+
ERROR_ACTIONS_COUNT("Action error count"),
141+
WEBSOCKET_CALLBACK_CLIENTS_COUNT("Websocket callback client count"),
142+
WEBSOCKET_CALLBACK_RESPONSE_HANDLERS_COUNT("Websocket callback response handler count"),
143+
WEBSOCKET_CALLBACK_FORWARD_HANDLERS_COUNT("Websocket callback forward handler count");
144+
145+
public final String description;
146+
147+
Name(String description) {
148+
this.description = description;
149+
}
115150
}
116151
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.mockserver.metrics;
2+
3+
import io.netty.channel.ChannelFutureListener;
4+
import io.netty.channel.ChannelHandlerContext;
5+
import io.netty.handler.codec.http.HttpHeaderNames;
6+
import io.prometheus.client.CollectorRegistry;
7+
import io.prometheus.client.exporter.common.TextFormat;
8+
import org.mockserver.configuration.Configuration;
9+
import org.mockserver.model.HttpRequest;
10+
import org.mockserver.model.HttpResponse;
11+
12+
import java.io.StringWriter;
13+
14+
import static org.mockserver.model.HttpResponse.notFoundResponse;
15+
import static org.mockserver.model.HttpResponse.response;
16+
17+
public class MetricsHandler {
18+
19+
private final Boolean metricsEnabled;
20+
21+
public MetricsHandler(Configuration configuration) {
22+
metricsEnabled = configuration.metricsEnabled();
23+
}
24+
25+
public void renderMetrics(final ChannelHandlerContext ctx, final HttpRequest request) throws Exception {
26+
HttpResponse response = notFoundResponse();
27+
if (metricsEnabled) {
28+
StringWriter stringWriter = new StringWriter();
29+
String contentType = TextFormat.chooseContentType(request.getFirstHeader("Accept"));
30+
TextFormat.writeFormat(contentType, stringWriter, CollectorRegistry.defaultRegistry.metricFamilySamples());
31+
String content = stringWriter.toString();
32+
response =
33+
response()
34+
.withHeader(HttpHeaderNames.CONTENT_TYPE.toString(), contentType)
35+
.withHeader(HttpHeaderNames.CONTENT_LENGTH.toString(), String.valueOf(content.getBytes().length))
36+
.withBody(content);
37+
}
38+
if (!request.isKeepAlive()) {
39+
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
40+
} else {
41+
ctx.writeAndFlush(response);
42+
}
43+
}
44+
45+
}

0 commit comments

Comments
 (0)
Please sign in to comment.