/* * Copyright 2014-2106 Netflix, 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.spark; import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.netflix.spectator.api.AbstractRegistry; import com.netflix.spectator.api.Clock; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.DistributionSummary; 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.Tag; import com.netflix.spectator.api.Timer; import org.apache.spark.SparkConf; import org.apache.spark.SparkEnv; import org.apache.spark.SparkEnv$; import scala.Option; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * Registry that reports values to a sidecar process via an HTTP call. */ public class SidecarRegistry extends AbstractRegistry { private static final Callable<Map<String, String>> SPARK = new Callable<Map<String, String>>() { @Override public Map<String, String> call() throws Exception { SparkEnv env = SparkEnv$.MODULE$.get(); if (env == null) { return Collections.emptyMap(); } else { Map<String, String> tagMap = new HashMap<>(); SparkConf conf = env.conf(); put(tagMap, conf, "spark.app.name", "appName"); return tagMap; } } private void put(Map<String, String> tags, SparkConf conf, String key, String tagName) { Option<String> opt = conf.getOption(key); if (opt.isDefined() && !"".equals(opt.get())) { tags.put(tagName, opt.get()); } } }; private ScheduledExecutorService executor; private final Counter numMessages; private final Counter numMeasurements; private final Callable<Map<String, String>> commonTags; /** Create a new instance. */ public SidecarRegistry() { this(Clock.SYSTEM); } /** Create a new instance. */ public SidecarRegistry(Clock clock) { this(clock, SPARK); } /** Create a new instance. */ public SidecarRegistry(Clock clock, Callable<Map<String, String>> commonTags) { super(clock); numMessages = counter(createId("spectator.sidecar.numMessages")); numMeasurements = counter(createId("spectator.sidecar.numMeasurements")); this.commonTags = commonTags; } /** * Start sending data to the sidecar. * * @param url * Location of the sidecar endpoint. * @param pollPeriod * How frequently to poll the data and send to the sidecar. * @param pollUnit * Unit for the {@code pollPeriod}. */ public void start(final URL url, long pollPeriod, TimeUnit pollUnit) { logger.info("starting sidecar registry with url {} and poll period {} {}", url, pollPeriod, pollUnit); executor = Executors.newSingleThreadScheduledExecutor( r -> { final Thread t = new Thread(r, "spectator-sidecar"); t.setDaemon(true); return t; } ); final SidecarRegistry self = this; Runnable task = () -> { try { List<Measurement> ms = new ArrayList<>(); for (Meter meter : self) { for (Measurement m : meter.measure()) { ms.add(m); } } postJson(url, ms); } catch (Exception e) { logger.error("failed to send data to sidecar", e); } }; executor.scheduleWithFixedDelay(task, pollPeriod, pollPeriod, pollUnit); } /** * Stop sending data to the sidecar and shutdown the executor. */ public void stop() { executor.shutdown(); executor = null; } private String toJson(List<Measurement> ms, Map<String, String> tags) { final JsonArray items = new JsonArray(); for (Measurement m : ms) { if (!Double.isNaN(m.value()) && !Double.isInfinite(m.value())) { items.add(toJson(m, tags)); } } return items.toString(); } private JsonObject toJson(Measurement m, Map<String, String> tags) { Map<String, String> tagMap = new HashMap<>(); for (Tag t : m.id().tags()) { tagMap.put(t.key(), t.value()); } tagMap.putAll(tags); final JsonObject obj = new JsonObject(); obj.add("timestamp", m.timestamp()); obj.add("type", getType(m.id())); obj.add("name", m.id().name()); obj.add("tags", toJson(tagMap)); obj.add("value", m.value()); return obj; } private JsonObject toJson(Map<String, String> tags) { final JsonObject obj = new JsonObject(); for (Map.Entry<String, String> entry : tags.entrySet()) { obj.add(entry.getKey(), entry.getValue()); } return obj; } private String getType(Id id) { for (Tag t : id.tags()) { if (t.key().equals("type")) { return t.value(); } } return DataType.GAUGE.value(); } private Map<String, String> getCommonTags() { try { return commonTags.call(); } catch (Exception e) { logger.warn("failed to determine common tags", e); return null; } } private void postJson(URL url, List<Measurement> ms) throws Exception { final Map<String, String> tags = getCommonTags(); if (!ms.isEmpty() && tags != null) { logger.info("sending {} messages to sidecar {} with tags {}", ms.size(), url.toString(), tags); numMessages.increment(); numMeasurements.increment(ms.size()); String json = toJson(ms, tags); HttpURLConnection con = (HttpURLConnection) url.openConnection(); try { con.setRequestMethod("POST"); con.setDoInput(true); con.setDoOutput(true); try (OutputStream out = con.getOutputStream()) { out.write(json.getBytes("UTF-8")); } con.connect(); int status = con.getResponseCode(); if (status != 200) { logger.error("post to sidecar failed with status: " + status + ", payload: " + json); } } finally { con.disconnect(); } } } @Override protected Counter newCounter(Id id) { return new SidecarCounter(clock(), id); } @Override protected DistributionSummary newDistributionSummary(Id id) { return new SidecarDistributionSummary(clock(), id); } @Override protected Timer newTimer(Id id) { return new SidecarTimer(clock(), id); } @Override protected Gauge newGauge(Id id) { return new SidecarGauge(clock(), id); } }