Skip to content

Commit

Permalink
Update PrometheusScrapeEndpoint implementation
Browse files Browse the repository at this point in the history
The Prometheus scrape endpoint was returning a 406 response when using
the Accept header "text/plain; version=0.0.4"

To resolve this, update the PrometheusScrapeEndpoint implementation to
be based on:
    https://github.com/spring-projects/spring-boot/blob/2.2.x/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java

Also:
- Add test to verify aop-prometheus endpoint responses
- Add spring-boot-test-autoconfigure test dependency to auto configure mock mvc
- Add gson test dependency to resolve conflict
  • Loading branch information
osoriano committed Mar 4, 2022
1 parent 6adb28e commit cb15f3b
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 41 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ subprojects { subProject ->
testImplementation "org.mockito:mockito-core:2.+"
testImplementation 'org.springframework:spring-test:5.1.7.RELEASE'
testImplementation 'org.springframework.boot:spring-boot-test:2.1.5.RELEASE'
testImplementation 'org.springframework.boot:spring-boot-test-autoconfigure:2.2.13.RELEASE'
testImplementation 'org.springframework.vault:spring-vault-core:2.1.2.RELEASE'
testImplementation(group: 'org.pf4j', name: 'pf4j', version: "${pf4jVersion}")
testImplementation(group: 'io.spinnaker.kork', name: 'kork-plugins-api', version: "${korkVersion}")
Expand All @@ -93,6 +94,7 @@ subprojects { subProject ->
testImplementation "org.mock-server:mockserver-junit-rule:5.10.0"
testImplementation "org.testcontainers:testcontainers:1.14.3"
testImplementation 'io.rest-assured:rest-assured:4.3.0'
testImplementation 'com.google.code.gson:gson:2.8.6'
}

group = "io.armory.plugins.metrics"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,21 @@

package io.armory.plugin.observability.prometheus;

import io.prometheus.client.Collector;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.common.TextFormat;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Enumeration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;

/**
* Port of PrometheusScrapeEndpoint but rather than being an web endpoint that won't work with
* plugins, it will be an endpoint
* Port of PrometheusScrapeEndpoint
*
* <p>See:
* https://github.com/spring-projects/spring-boot/blob/cd1baf18fe9ec71c11d7d131d6f1a417ec0c00e2/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java
* https://github.com/spring-projects/spring-boot/blob/2.2.x/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java
*/
// If you use WebEndpoint instead of Endpoint, the plugin throws class def not found error with PF4j
// ¯\_(ツ)_/¯
@Endpoint(id = "aop-prometheus")
@WebEndpoint(id = "aop-prometheus")
public class PrometheusScrapeEndpoint {

private final CollectorRegistry collectorRegistry;
Expand All @@ -47,22 +39,12 @@ public PrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) {
this.collectorRegistry = collectorRegistry;
}

// If you use produces = TextFormat.CONTENT_TYPE_004, for some reason the accept header is ignored
// and a 406 is returned :/
// So we will use ResponseEntity<String> and set the content type manually.
@ReadOperation()
public ResponseEntity<String> scrape() {
@ReadOperation(produces = TextFormat.CONTENT_TYPE_004)
public String scrape() {
try {
Writer writer = new StringWriter();
Enumeration<Collector.MetricFamilySamples> samples =
this.collectorRegistry.metricFamilySamples();
TextFormat.write004(writer, samples);

var responseHeaders = new HttpHeaders();
responseHeaders.set("Content-Type", TextFormat.CONTENT_TYPE_004);
TextFormat.write004(writer, samples);

return new ResponseEntity<>(writer.toString(), responseHeaders, HttpStatus.OK);
TextFormat.write004(writer, this.collectorRegistry.metricFamilySamples());
return writer.toString();
} catch (IOException ex) {
// This actually never happens since StringWriter::write() doesn't throw any
// IOException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package io.armory.plugin.observability.prometheus;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

Expand Down Expand Up @@ -74,13 +73,8 @@ public void test_that_scrape_returns_the_expected_serialized_response() throws E
.getResource("io/armory/plugin/observability/prometheus/expected-scrape.txt");
var expectedContent = Files.readString(Path.of(expectedScrapeResource.toURI()));

var responseEntity = sut.scrape();
assertTrue(responseEntity.getStatusCode().is2xxSuccessful());
//noinspection ConstantConditions
assertEquals(
"text/plain;version=0.0.4;charset=utf-8",
responseEntity.getHeaders().getContentType().toString());
assertEquals(expectedContent, responseEntity.getBody());
var response = sut.scrape();
assertEquals(expectedContent, response);
}

/**
Expand All @@ -105,16 +99,11 @@ public void test_that_the_prometheus_registry_can_handle_tags_that_are_sometimes

registry.counter("foo", tagsWithMissingTag).increment();
registry.counter("foo", fullCollectionOfTags).increment();
var responseEntity = sut.scrape();
assertTrue(responseEntity.getStatusCode().is2xxSuccessful());
//noinspection ConstantConditions
assertEquals(
"text/plain;version=0.0.4;charset=utf-8",
responseEntity.getHeaders().getContentType().toString());
var response = sut.scrape();
// use length since, order is non-deterministic
// Since multiple HELP/TYPE will be removed we are expecting the length to be equal to
// the original - length(chars to be removed)
String duplicate = "# HELP foo_total \n# TYPE foo_total counter ";
assertEquals(expectedContent.length() - duplicate.length(), responseEntity.getBody().length());
assertEquals(expectedContent.length() - duplicate.length(), response.length());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2022 Armory, 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 io.armory.plugin.observability.prometheus;

import io.armory.plugin.observability.model.PluginConfig;
import io.armory.plugin.observability.model.SecurityConfig;
import io.prometheus.client.exporter.common.TextFormat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@RunWith(SpringRunner.class)
@SpringBootTest(
classes = {
PrometheusScrapeEndpointIntegrationTest.TestSpringApplication.class,
PrometheusScrapeEndpoint.class,
SecurityConfig.class,
PluginConfig.class
},
properties = {
"management.endpoints.web.exposure.include=*",
"spinnaker.extensibility.plugins.armory.observability-plugin.config.metrics.prometheus.enabled=true"
})
@AutoConfigureMockMvc
public class PrometheusScrapeEndpointIntegrationTest {

@Autowired private MockMvc mock;

@Test
public void testRequest() throws Exception {
mock.perform(MockMvcRequestBuilders.get("/aop-prometheus"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(
MockMvcResultMatchers.content().contentTypeCompatibleWith(TextFormat.CONTENT_TYPE_004))
.andReturn();
}

@Test
public void testRequestWithAcceptTextPlain() throws Exception {
mock.perform(MockMvcRequestBuilders.get("/aop-prometheus").accept(MediaType.TEXT_PLAIN))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().contentTypeCompatibleWith(MediaType.TEXT_PLAIN))
.andReturn();
}

@Test
public void testRequestWithAcceptTextPlain004() throws Exception {
mock.perform(MockMvcRequestBuilders.get("/aop-prometheus").accept(TextFormat.CONTENT_TYPE_004))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(
MockMvcResultMatchers.content().contentTypeCompatibleWith(TextFormat.CONTENT_TYPE_004))
.andReturn();
}

@SpringBootApplication
public static class TestSpringApplication {}
}

0 comments on commit cb15f3b

Please sign in to comment.