/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 gobblin.metrics.graphite;
import java.io.IOException;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;
import gobblin.configuration.ConfigurationKeys;
import gobblin.metrics.GobblinTrackingEvent;
import gobblin.metrics.MetricContext;
import gobblin.metrics.event.MultiPartEvent;
import gobblin.metrics.event.EventSubmitter;
import gobblin.metrics.event.JobEvent;
import gobblin.metrics.event.TaskEvent;
import gobblin.metrics.reporter.EventReporter;
import static gobblin.metrics.event.TimingEvent.METADATA_DURATION;
/**
*
* {@link gobblin.metrics.reporter.EventReporter} that emits {@link gobblin.metrics.GobblinTrackingEvent} events
* as timestamped name - value pairs through the Graphite protocol
*
* @author Lorand Bendig
*
*/
public class GraphiteEventReporter extends EventReporter {
private final GraphitePusher graphitePusher;
private final boolean emitValueAsKey;
private static final String EMTPY_VALUE = "0";
private static final Logger LOGGER = LoggerFactory.getLogger(GraphiteEventReporter.class);
public GraphiteEventReporter(Builder<?> builder) throws IOException {
super(builder);
if (builder.graphitePusher.isPresent()) {
this.graphitePusher = builder.graphitePusher.get();
} else {
this.graphitePusher =
this.closer.register(new GraphitePusher(builder.hostname, builder.port, builder.connectionType));
}
this.emitValueAsKey = builder.emitValueAsKey;
}
@Override
public void reportEventQueue(Queue<GobblinTrackingEvent> queue) {
GobblinTrackingEvent nextEvent;
try {
while (null != (nextEvent = queue.poll())) {
pushEvent(nextEvent);
}
this.graphitePusher.flush();
} catch (IOException e) {
LOGGER.error("Error sending event to Graphite", e);
try {
this.graphitePusher.flush();
} catch (IOException e1) {
LOGGER.error("Unable to flush previous events to Graphite", e);
}
}
}
/**
* Extracts the event and its metadata from {@link GobblinTrackingEvent} and creates
* timestamped name value pairs
*
* @param event {@link GobblinTrackingEvent} to be reported
* @throws IOException
*/
private void pushEvent(GobblinTrackingEvent event) throws IOException {
Map<String, String> metadata = event.getMetadata();
String name = getMetricName(metadata, event.getName());
long timestamp = event.getTimestamp() / 1000l;
MultiPartEvent multipartEvent = MultiPartEvent.getEvent(metadata.get(EventSubmitter.EVENT_TYPE));
if (multipartEvent == null) {
graphitePusher.push(name, EMTPY_VALUE, timestamp);
}
else {
for (String field : multipartEvent.getMetadataFields()) {
String value = metadata.get(field);
if (value == null) {
graphitePusher.push(JOINER.join(name, field), EMTPY_VALUE, timestamp);
} else {
if (emitAsKey(field)) {
// metric value is emitted as part of the keys
graphitePusher.push(JOINER.join(name, field, value), EMTPY_VALUE, timestamp);
} else {
graphitePusher.push(JOINER.join(name, field), convertValue(field, value), timestamp);
}
}
}
}
}
private String convertValue(String field, String value) {
return METADATA_DURATION.equals(field) ?
Double.toString(convertDuration(TimeUnit.MILLISECONDS.toNanos(Long.parseLong(value)))) : value;
}
/**
* Non-numeric event values may be emitted as part of the key by applying them to the end of the key if
* {@link ConfigurationKeys#METRICS_REPORTING_GRAPHITE_EVENT_VALUE_AS_KEY} is set. Thus such events can be still
* reported even when the backend doesn't accept text values through Graphite
*
* @param field name of the metric's metadata fields
* @return true if event value is emitted in the key
*/
private boolean emitAsKey(String field) {
return emitValueAsKey
&& (field.equals(TaskEvent.METADATA_TASK_WORKING_STATE) || field.equals(JobEvent.METADATA_JOB_STATE));
}
/**
* Returns a new {@link GraphiteEventReporter.Builder} for {@link GraphiteEventReporter}.
* Will automatically add all Context tags to the reporter.
*
* @param context the {@link gobblin.metrics.MetricContext} to report
* @return GraphiteEventReporter builder
* @deprecated this method is bugged. Use {@link GraphiteEventReporter.Factory#forContext} instead.
*/
@Deprecated
public static Builder<? extends Builder> forContext(MetricContext context) {
return new BuilderImpl(context);
}
public static class BuilderImpl extends Builder<BuilderImpl> {
private BuilderImpl(MetricContext context) {
super(context);
}
@Override
protected BuilderImpl self() {
return this;
}
}
public static class Factory {
/**
* Returns a new {@link GraphiteEventReporter.Builder} for {@link GraphiteEventReporter}.
* Will automatically add all Context tags to the reporter.
*
* @param context the {@link gobblin.metrics.MetricContext} to report
* @return GraphiteEventReporter builder
*/
public static BuilderImpl forContext(MetricContext context) {
return new BuilderImpl(context);
}
}
/**
* Builder for {@link GraphiteEventReporter}.
* Defaults to no filter, reporting rates in seconds and times in milliseconds using TCP connection
*/
public static abstract class Builder<T extends EventReporter.Builder<T>> extends EventReporter.Builder<T> {
protected String hostname;
protected int port;
protected GraphiteConnectionType connectionType;
protected Optional<GraphitePusher> graphitePusher;
protected boolean emitValueAsKey;
protected Builder(MetricContext context) {
super(context);
this.graphitePusher = Optional.absent();
this.connectionType = GraphiteConnectionType.TCP;
}
/**
* Set {@link gobblin.metrics.graphite.GraphitePusher} to use.
*/
public T withGraphitePusher(GraphitePusher pusher) {
this.graphitePusher = Optional.of(pusher);
return self();
}
/**
* Set connection parameters for the {@link gobblin.metrics.graphite.GraphitePusher} creation
*/
public T withConnection(String hostname, int port) {
this.hostname = hostname;
this.port = port;
return self();
}
/**
* Set {@link gobblin.metrics.graphite.GraphiteConnectionType} to use.
*/
public T withConnectionType(GraphiteConnectionType connectionType) {
this.connectionType = connectionType;
return self();
}
/**
* Set flag that forces the reporter to emit non-numeric event values as part of the key
*/
public T withEmitValueAsKey(boolean emitValueAsKey) {
this.emitValueAsKey = emitValueAsKey;
return self();
}
/**
* Builds and returns {@link GraphiteEventReporter}.
*
* @return GraphiteEventReporter
*/
public GraphiteEventReporter build() throws IOException {
return new GraphiteEventReporter(this);
}
}
}