/*
* 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.addthis.hydra.task.output;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.addthis.bundle.core.Bundle;
import com.addthis.bundle.util.AutoField;
import com.addthis.bundle.value.ValueObject;
import com.addthis.codec.annotations.Time;
import com.addthis.codec.jackson.Jackson;
import com.addthis.metrics.reporter.config.GmondConfigParser;
import com.addthis.metrics.reporter.config.HostPort;
import com.google.common.base.Objects;
import com.google.common.base.Throwables;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkState;
import info.ganglia.gmetric4j.gmetric.GMetric;
import static info.ganglia.gmetric4j.gmetric.GMetric.UDPAddressingMode.UNICAST;
import info.ganglia.gmetric4j.gmetric.GMetricSlope;
import info.ganglia.gmetric4j.gmetric.GMetricType;
import info.ganglia.gmetric4j.gmetric.GangliaException;
public final class GangliaOutput extends AbstractFilteredOutput {
private static final Logger log = LoggerFactory.getLogger(GangliaOutput.class);
private final List<HostPort> hosts;
private final int tMax;
private final int dMax;
private final AutoField name;
private final AutoField value;
private final AutoField group;
private final AutoField units;
@Nullable private transient List<GMetric> gmetrics;
@JsonCreator
private GangliaOutput(@JsonProperty("hosts") GangliaHosts hosts,
@JsonProperty("tMax") @Time(TimeUnit.SECONDS) int tMax,
@JsonProperty("dMax") @Time(TimeUnit.SECONDS) int dMax,
@JsonProperty("name") AutoField name,
@JsonProperty("value") AutoField value,
@JsonProperty("group") AutoField group,
@JsonProperty("units") AutoField units) {
this.hosts = hosts.gangliaHosts;
this.tMax = tMax;
this.dMax = dMax;
this.name = name;
this.value = value;
this.group = group;
this.units = units;
}
@Override
protected void open() {
super.open();
checkState(gmetrics == null, "open was already called");
log.info("opening ganglia output with hosts: {}", hostsToString());
gmetrics = hosts.stream().map(hostPort -> {
try {
return new GMetric(hostPort.getHost(), hostPort.getPort(), UNICAST, 1);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}).collect(Collectors.toList());
}
// only needed because metrics-reporter-config did not write a toString method for HostPort
private String hostsToString() {
try {
return Jackson.defaultMapper().writeValueAsString(hosts);
} catch (JsonProcessingException e) {
return e.getMessage();
}
}
@Override public void send(Bundle bundle) {
checkState(gmetrics != null, "output is either already closed or was never opened");
if (!filter(bundle)) {
return;
}
try {
announce(name.getValue(bundle).toString(),
group.getValue(bundle).toString(),
value.getValue(bundle),
units.getValue(bundle).toString());
} catch (GangliaException e) {
throw Throwables.propagate(e);
}
}
private void announce(String metricName, String metricGroup, ValueObject metricValue, String metricUnits)
throws GangliaException {
// see if we have to represent a long as a double
if (metricValue.getObjectType() == ValueObject.TYPE.INT) {
long asLong = metricValue.asLong().getLong();
if ((int) asLong != asLong) {
metricValue = metricValue.asDouble();
}
}
for (GMetric gmetric : gmetrics) {
gmetric.announce(metricName, metricValue.toString(), detectType(metricValue.getObjectType()),
metricUnits, GMetricSlope.BOTH, tMax, dMax, metricGroup);
}
}
private static GMetricType detectType(ValueObject.TYPE o) {
switch (o) {
case FLOAT: return GMetricType.DOUBLE;
case INT: return GMetricType.INT32;
default: return GMetricType.STRING;
}
}
@Override public void sendComplete() {
checkState(gmetrics != null, "output is either already closed or was never opened");
closeGmetrics();
}
// note: this method's semantics are at best unclear, so we just try to do the best was can
@Override public void sourceError(Throwable cause) {
log.error("source error reported; closing output in response", cause);
if (gmetrics != null) {
closeGmetrics();
}
}
private void closeGmetrics() {
assert gmetrics != null : "should only be called after ensuring gmetrics is not null";
for (GMetric gmetric : gmetrics) {
try {
gmetric.close();
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
gmetrics = null;
}
@Override public String toString() {
return Objects.toStringHelper(this)
.add("hosts", hostsToString())
.add("tMax", tMax)
.add("dMax", dMax)
.add("name", name)
.add("value", value)
.add("group", group)
.add("units", units)
.add("gmetrics", gmetrics)
.toString();
}
/** Exists only to support both file-name and host-port-list deserialization via a single field. */
private static final class GangliaHosts {
private final List<HostPort> gangliaHosts;
private GangliaHosts(String fileName) {
this.gangliaHosts = new GmondConfigParser().getGmondSendChannels(fileName);
}
@JsonCreator
private GangliaHosts(List<HostPort> gangliaHosts) {
this.gangliaHosts = gangliaHosts;
}
}
}