Skip to content

Commit

Permalink
feat(prometheus): Initial step at plumbing in prometheus (#66)
Browse files Browse the repository at this point in the history
Here is an example configuration for kayenta-local.yml
  prometheus:
    enabled: true
    endpoint:
      baseUrl: http://localhost:9090

    accounts:
      - name: my-prometheus-server
        supportedTypes:
          - METRICS_STORE

See the comment in PrometheusConfigurationProperties regarding configuring
the scopeLabel.

This still needs a PR to change the extension labels in order to propagate filters
and "sumBy" fields from canaryController calls. However everything else is plumbed
up. Without passing those fields through, alignment is unlikely because there is
too much context to align (e.g. individual CPU cores are treated independent
rather than summing into a single aggregated value)
  • Loading branch information
Eric Wiseblatt committed Aug 22, 2017
1 parent 5561e54 commit ef52c30
Show file tree
Hide file tree
Showing 21 changed files with 1,114 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.netflix.kayenta.canary.providers.AtlasCanaryMetricSetQueryConfig;
import com.netflix.kayenta.canary.providers.PrometheusCanaryMetricSetQueryConfig;
import com.netflix.kayenta.canary.providers.StackdriverCanaryMetricSetQueryConfig;

@JsonTypeInfo(use= JsonTypeInfo.Id.NAME, include= JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({@JsonSubTypes.Type(value = AtlasCanaryMetricSetQueryConfig.class, name = "atlas"),
@JsonSubTypes.Type(value = PrometheusCanaryMetricSetQueryConfig.class, name = "prometheus"),
@JsonSubTypes.Type(value = StackdriverCanaryMetricSetQueryConfig.class, name = "stackdriver")})
@JsonInclude(JsonInclude.Include.NON_NULL)
public interface CanaryMetricSetQueryConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2017 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.kayenta.canary.providers;

import com.netflix.kayenta.canary.CanaryMetricSetQueryConfig;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.validation.constraints.NotNull;
import java.util.List;

@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
// TODO(duftler): Figure out how to move this into the kayenta-prometheus module? Doing so as-is would introduce a circular dependency.
public class PrometheusCanaryMetricSetQueryConfig implements CanaryMetricSetQueryConfig {

@NotNull
@Getter
private String metricName;

@Getter
private String aggregationPeriod;

@Getter
private String instancePattern;

@Getter
private List<String> labelBindings;

@Getter
private List<String> sumByFields;
}
14 changes: 14 additions & 0 deletions kayenta-prometheus/kayenta-prometheus.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
dependencies {
compile project(":kayenta-core")

// compile spinnaker.dependency('bootWeb')
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"

// compile spinnaker.dependency("korkSwagger")
compile "com.netflix.spinnaker.kork:kork-swagger:$korkVersion"

// compile spinnaker.dependency('lombok')
compile "org.projectlombok:lombok:1.16.10"

compile "com.netflix.spinnaker.orca:orca-core:$orcaVersion"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2017 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.kayenta.prometheus.canary;

import com.netflix.kayenta.canary.CanaryScope;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

import javax.validation.constraints.NotNull;

@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PrometheusCanaryScope extends CanaryScope {

@NotNull
private String type;

@NotNull
private String intervalStartTimeIso;

@NotNull
private String intervalEndTimeIso;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2017 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.kayenta.prometheus.canary;

import com.netflix.kayenta.canary.CanaryScope;
import com.netflix.kayenta.canary.CanaryScopeFactory;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.Map;

@Component
public class PrometheusCanaryScopeFactory implements CanaryScopeFactory {

@Override
public boolean handles(String serviceType) {
return "prometheus".equals(serviceType);
}

@Override
public CanaryScope buildCanaryScope(String scope,
Instant startTimeInstant,
Instant endTimeInstant,
String step,
Map<String, String> extendedScopeParams) {
PrometheusCanaryScope prometheusCanaryScope = new PrometheusCanaryScope();
prometheusCanaryScope.setScope(scope);
prometheusCanaryScope.setStart(startTimeInstant.toEpochMilli() + "");
prometheusCanaryScope.setEnd(endTimeInstant.toEpochMilli() + "");
prometheusCanaryScope.setIntervalStartTimeIso(startTimeInstant + "");
prometheusCanaryScope.setIntervalEndTimeIso(endTimeInstant + "");
prometheusCanaryScope.setStep(step);

if (extendedScopeParams != null && extendedScopeParams.containsKey("type")) {
prometheusCanaryScope.setType(extendedScopeParams.get("type"));
}

return prometheusCanaryScope;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2017 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.kayenta.prometheus.config;

import com.netflix.kayenta.prometheus.metrics.PrometheusMetricsService;
import com.netflix.kayenta.prometheus.security.PrometheusCredentials;
import com.netflix.kayenta.prometheus.security.PrometheusNamedAccountCredentials;
import com.netflix.kayenta.prometheus.service.PrometheusRemoteService;
import com.netflix.kayenta.metrics.MetricsService;
import com.netflix.kayenta.retrofit.config.RetrofitClientFactory;
import com.netflix.kayenta.security.AccountCredentials;
import com.netflix.kayenta.security.AccountCredentialsRepository;
import com.squareup.okhttp.OkHttpClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.List;

@Configuration
@EnableConfigurationProperties
@ConditionalOnProperty("kayenta.prometheus.enabled")
@ComponentScan({"com.netflix.kayenta.prometheus"})
@Slf4j
public class PrometheusConfiguration {

@Bean
@ConfigurationProperties("kayenta.prometheus")
PrometheusConfigurationProperties prometheusConfigurationProperties() {
return new PrometheusConfigurationProperties();
}

// This is following Atlas pattern, which uses a single global service.
// However, I might rather have an endpoint per account or some list of services if monitoring is partitioned
// across multiple prometheus servers. I dont know how to wire that up in Spring unless I were to change
// the RemoteService to a facade that used an endpoint from the request (either injected or from credentials used).
@Bean
MetricsService prometheusMetricsService(PrometheusConfigurationProperties prometheusConfigurationProperties,
AccountCredentialsRepository accountCredentialsRepository,
PrometheusRemoteService prometheusRemoteService) throws IOException {
PrometheusMetricsService.PrometheusMetricsServiceBuilder prometheusMetricsServiceBuilder = PrometheusMetricsService.builder();
prometheusMetricsServiceBuilder.scopeLabel(prometheusConfigurationProperties.getScopeLabel());

for (PrometheusManagedAccount prometheusManagedAccount : prometheusConfigurationProperties.getAccounts()) {
String name = prometheusManagedAccount.getName();
List<AccountCredentials.Type> supportedTypes = prometheusManagedAccount.getSupportedTypes();

log.info("Registering Prometheus account {} with supported types {}.", name, supportedTypes);

PrometheusCredentials prometheusCredentials =
PrometheusCredentials
.builder()
.build();
PrometheusNamedAccountCredentials.PrometheusNamedAccountCredentialsBuilder prometheusNamedAccountCredentialsBuilder =
PrometheusNamedAccountCredentials
.builder()
.name(name)
.credentials(prometheusCredentials);

if (!CollectionUtils.isEmpty(supportedTypes)) {
if (supportedTypes.contains(AccountCredentials.Type.METRICS_STORE)) {
prometheusNamedAccountCredentialsBuilder.prometheusRemoteService(prometheusRemoteService);
}

prometheusNamedAccountCredentialsBuilder.supportedTypes(supportedTypes);
}

PrometheusNamedAccountCredentials prometheusNamedAccountCredentials = prometheusNamedAccountCredentialsBuilder.build();
accountCredentialsRepository.save(name, prometheusNamedAccountCredentials);
prometheusMetricsServiceBuilder.accountName(name);
}

PrometheusMetricsService prometheusMetricsService = prometheusMetricsServiceBuilder.build();

log.info("Populated PrometheusMetricsService with {} Prometheus accounts.", prometheusMetricsService.getAccountNames().size());

return prometheusMetricsService;
}

@Bean
PrometheusRemoteService prometheusRemoteService(PrometheusResponseConverter prometheusConverter,
PrometheusConfigurationProperties prometheusConfigurationProperties,
RetrofitClientFactory retrofitClientFactory,
OkHttpClient okHttpClient) {
return retrofitClientFactory.createClient(PrometheusRemoteService.class,
prometheusConverter,
prometheusConfigurationProperties.getEndpoint(),
okHttpClient);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2017 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.kayenta.prometheus.config;

import com.netflix.kayenta.retrofit.config.RemoteService;
import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;

// TODO(ewiseblatt):
// @Data this class instead of @Getter/Setter and final the "accounts".
public class PrometheusConfigurationProperties {

/**
* TODO(ewiseblatt):
* We're saying "instance" here by default, which is built-in to Prometheus.
* However, in practice this should be overriden to "host" where "host"
* is injected on scrape by the prometheus.yml configuration. The install
* for prometheus when using the --gce option does this, but that is the
* only configuration that does at this time.
*
* The difference is that prometheus adds "instance" as the __address__,
* which is the <host>:<port> but the <host> is typically an IP address.
* Instance can be explicitly overriden as well, but should be the particular
* service endpoint.
*
* In general, you do want the particular service endpoint in order to get
* the particular service of interest. In the case of looking for node_cpu,
* which is the default, then you would need to add the node_exporter service
* to the request (i.e. instance is <host>:9100). Using the above "host", this
* would collect *all* node_cpu from that host. However we assume that only
* the node_exporter is exporting "node_cpu" so there is only one (port 9100).
*
* I need to clean this up. Perhaps by configuring default prometheus to use
* the dns name instead of the IP in general for the __address__. Regardless,
* the application scraping can determine its own "instance" value (as well
* as "host") in which case the operator might need to compensate in how they
* configure this scopeLabel value and query using it.
*/
@Getter
@Setter
private String scopeLabel = "instance";

// Location of prometheus server.
@NotNull
@Getter
@Setter
private RemoteService endpoint;

@Getter
private List<PrometheusManagedAccount> accounts = new ArrayList<>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2017 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.kayenta.prometheus.config;

import com.netflix.kayenta.security.AccountCredentials;
import lombok.Data;

import javax.validation.constraints.NotNull;
import java.util.List;

@Data
public class PrometheusManagedAccount {

@NotNull
private String name;

private List<AccountCredentials.Type> supportedTypes;
}
Loading

0 comments on commit ef52c30

Please sign in to comment.