/*
* Copyright (c) 2015 Spotify AB.
*
* 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 com.spotify.heroic.metric;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import lombok.Data;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.concurrent.TimeUnit;
@JsonSerialize(using = QueryTrace.Serializer.class)
@JsonDeserialize(using = QueryTrace.Deserializer.class)
public interface QueryTrace {
Identifier PASSIVE_IDENTIFIER = new Identifier("NO TRACE");
PassiveTrace PASSIVE = new PassiveTrace();
Watch PASSIVE_WATCH = new PassiveWatch();
NamedWatch PASSIVE_NAMED_WATCH = new PassiveNamedWatch();
Joiner PASSIVE_JOINER = new PassiveJoiner();
/**
* Create an active query trace.
*
* @param what Identifier of the trace
* @return a {@link com.spotify.heroic.metric.QueryTrace}
* @deprecated use {@link #watch(com.spotify.heroic.metric.QueryTrace.Identifier)}
*/
static QueryTrace of(final Identifier what) {
return new ActiveTrace(what, 0L, ImmutableList.of());
}
/**
* Create an active query trace.
*
* @param what Identifier of the trace
* @param elapsed How long the query trace has elapsed for
* @return a {@link com.spotify.heroic.metric.QueryTrace}
* @deprecated use {@link #watch(com.spotify.heroic.metric.QueryTrace.Identifier)}
*/
static QueryTrace of(final Identifier what, final long elapsed) {
return new ActiveTrace(what, elapsed, ImmutableList.of());
}
/**
* Create an active query trace.
*
* @param what Identifier of the trace
* @param elapsed How long the query trace has elapsed for
* @param children Children of the query trace
* @return a {@link com.spotify.heroic.metric.QueryTrace}
* @deprecated use {@link #watch(com.spotify.heroic.metric.QueryTrace.Identifier)}
*/
static QueryTrace of(
final Identifier what, final long elapsed, final List<QueryTrace> children
) {
return new ActiveTrace(what, elapsed, children);
}
/**
* Create a new identifier for a class.
*
* @param where Class associated with the identifier
* @return an {@link com.spotify.heroic.metric.QueryTrace.Identifier}
*/
static Identifier identifier(Class<?> where) {
return new Identifier(where.getName());
}
/**
* Create a new identifier for a class and a what
*
* @param where Class associated with the identifier
* @param what String describing what is being traced
* @return an {@link com.spotify.heroic.metric.QueryTrace.Identifier}
*/
static Identifier identifier(Class<?> where, String what) {
return new Identifier(where.getName() + "#" + what);
}
/**
* Create a new identifier for a free text description.
*
* @param description Description telling what is being traced.
* @return an {@link com.spotify.heroic.metric.QueryTrace.Identifier}
*/
static Identifier identifier(String description) {
return new Identifier(description);
}
/**
* Create a new watch.
*
* @return a {@link com.spotify.heroic.metric.QueryTrace.Watch}
* @deprecated use {@link Tracing#watch()}
*/
static Watch watch() {
return new ActiveWatch(Stopwatch.createStarted());
}
/**
* Create a new watch.
*
* @return a {@link com.spotify.heroic.metric.QueryTrace.Watch}
* @deprecated use {@link Tracing#watch(Identifier)}
*/
static NamedWatch watch(final Identifier what) {
return new ActiveNamedWatch(what, Stopwatch.createStarted());
}
/**
* Format the current trace onto the given PrintWriter.
*
* @param out {@link java.io.PrintWriter} to format to
*/
void formatTrace(PrintWriter out);
/**
* Format the current trace onto the given PrintWriter with the given prefix.
*
* @param out {@link java.io.PrintWriter} to format to
* @param prefix prefix to prepend
*/
void formatTrace(PrintWriter out, String prefix);
/**
* @deprecated use {@link #elapsed()}
*/
@Deprecated
long getElapsed();
/**
* @deprecated use {@link #what()}
*/
@Deprecated
Identifier getWhat();
/**
* @deprecated use {@link #children()}
*/
@Deprecated
List<QueryTrace> getChildren();
/**
* How long the trace elapsed for.
*
* @return microseconds
*/
long elapsed();
/**
* Get the identifier for this trace.
*
* @return an identifier
*/
Identifier what();
/**
* Get a list of all immediate children to this trace.
*
* @return a list
*/
List<QueryTrace> children();
@JsonTypeName("passive")
@Data
class PassiveTrace implements QueryTrace {
PassiveTrace() {
}
@JsonCreator
public static PassiveTrace create() {
return PASSIVE;
}
@Override
public void formatTrace(final PrintWriter out) {
formatTrace(out, "");
}
@Override
public void formatTrace(final PrintWriter out, final String prefix) {
out.println(prefix + "NO TRACE");
}
@JsonIgnore
@Override
public long getElapsed() {
return elapsed();
}
@JsonIgnore
@Override
public Identifier getWhat() {
return what();
}
@JsonIgnore
@Override
public List<QueryTrace> getChildren() {
return children();
}
@Override
public long elapsed() {
return 0L;
}
@Override
public Identifier what() {
return PASSIVE_IDENTIFIER;
}
@Override
public List<QueryTrace> children() {
return ImmutableList.of();
}
}
@JsonTypeName("active")
@Data
class ActiveTrace implements QueryTrace {
private final Identifier what;
private final long elapsed;
private final List<QueryTrace> children;
@Override
public void formatTrace(PrintWriter out) {
formatTrace(out, "");
}
@Override
public void formatTrace(PrintWriter out, String prefix) {
out.println(prefix + what + " (in " + readableTime(elapsed) + ")");
for (final QueryTrace child : children) {
child.formatTrace(out, prefix + " ");
}
}
@Override
public long elapsed() {
return elapsed;
}
@Override
public Identifier what() {
return what;
}
@Override
public List<QueryTrace> children() {
return children;
}
private String readableTime(long elapsed) {
if (elapsed > 1000000000) {
return String.format("%.3fs", elapsed / 1000000000d);
}
if (elapsed > 1000000) {
return String.format("%.3fms", elapsed / 1000000d);
}
if (elapsed > 1000) {
return String.format("%.3fus", elapsed / 1000d);
}
return elapsed + "ns";
}
/**
* Convert to model.
*
* @return a new json model
*/
ActiveJsonModel toModel() {
return new ActiveJsonModel(what, elapsed, children);
}
}
interface Watch {
/**
* End the current watch and return a trace.
* <p>
* The same watch can be used multiple times, weven when ended.
*
* @param what The thing that is being traced
* @return a {@link com.spotify.heroic.metric.QueryTrace}
*/
QueryTrace end(final Identifier what);
/**
* End the current watch and return a trace with the given child.
* <p>
* The same watch can be used multiple times, weven when ended.
*
* @param what The thing that is being traced
* @param child Child to add to the new {@link com.spotify.heroic.metric.QueryTrace}
* @return a {@link com.spotify.heroic.metric.QueryTrace}
*/
QueryTrace end(final Identifier what, final QueryTrace child);
/**
* End the current watch and return a trace with the given children.
* <p>
* The same watch can be used multiple times, weven when ended.
*
* @param what The thing that is being traced
* @param children Children to add to the new {@link com.spotify.heroic.metric.QueryTrace}
* @return a {@link com.spotify.heroic.metric.QueryTrace}
*/
QueryTrace end(final Identifier what, final List<QueryTrace> children);
/**
* How long this trace has elapsed for.
*
* @return microseconds
* @deprecated Makes not distinction between passive and active watches
*/
long elapsed();
}
/**
* A watch that is measuring.
*/
@Data
class ActiveWatch implements Watch {
private final Stopwatch w;
@Override
public QueryTrace end(final Identifier what) {
return new ActiveTrace(what, elapsed(), ImmutableList.of());
}
@Override
public QueryTrace end(final Identifier what, final QueryTrace child) {
return new ActiveTrace(what, elapsed(), ImmutableList.of(child));
}
@Override
public QueryTrace end(final Identifier what, final List<QueryTrace> children) {
return new ActiveTrace(what, elapsed(), children);
}
@Override
public long elapsed() {
return w.elapsed(TimeUnit.MICROSECONDS);
}
}
/**
* A watch that is not measuring.
*/
@Data
class PassiveWatch implements Watch {
@Override
public QueryTrace end(final Identifier what) {
return PASSIVE;
}
@Override
public QueryTrace end(final Identifier what, final QueryTrace child) {
return PASSIVE;
}
@Override
public QueryTrace end(final Identifier what, final List<QueryTrace> children) {
return PASSIVE;
}
@Override
public long elapsed() {
return 0L;
}
}
interface NamedWatch {
/**
* End the current watch and return a trace.
* <p>
* The same watch can be used multiple times, weven when ended.
*
* @return a {@link com.spotify.heroic.metric.QueryTrace}
*/
QueryTrace end();
/**
* End the current watch and return a trace with the given child.
* <p>
* The same watch can be used multiple times, weven when ended.
*
* @param child Child to add to the new {@link com.spotify.heroic.metric.QueryTrace}
* @return a {@link com.spotify.heroic.metric.QueryTrace}
*/
QueryTrace end(QueryTrace child);
/**
* End the current watch and return a trace with the given children.
* <p>
* The same watch can be used multiple times, weven when ended.
*
* @param children Children to add to the new {@link com.spotify.heroic.metric.QueryTrace}
* @return a {@link com.spotify.heroic.metric.QueryTrace}
*/
QueryTrace end(List<QueryTrace> children);
/**
* Create a joiner from the current watch.
*
* @return a {@link com.spotify.heroic.metric.QueryTrace.Joiner}
*/
Joiner joiner();
/**
* Create another watch with the same configuration as this watch (active or passive).
*
* @param what What is being watched
* @return a {@link com.spotify.heroic.metric.QueryTrace.NamedWatch}
*/
NamedWatch watch(Identifier what);
/**
* Create another watch that copies everything from this watch, while extending the id.
*
* @param appendName What to extend the identifier with
* @return a {@link com.spotify.heroic.metric.QueryTrace.NamedWatch}
*/
NamedWatch extendIdentifier(String appendName);
/**
* How long this trace has elapsed for.
*
* @return milliseconds since unix epoch
* @deprecated Makes no distinction between active and passive watches.
*/
long elapsed();
}
@Data
class ActiveNamedWatch implements NamedWatch {
private final Identifier what;
private final Stopwatch w;
@Override
public QueryTrace end() {
return new ActiveTrace(what, elapsed(), ImmutableList.of());
}
@Override
public QueryTrace end(final QueryTrace child) {
return new ActiveTrace(what, elapsed(), ImmutableList.of(child));
}
@Override
public QueryTrace end(final List<QueryTrace> children) {
return new ActiveTrace(what, elapsed(), children);
}
@Override
public Joiner joiner() {
return new ActiveJoiner(this);
}
@Override
public NamedWatch watch(final Identifier what) {
return new ActiveNamedWatch(what, Stopwatch.createStarted());
}
@Override
public NamedWatch extendIdentifier(String appendName) {
return new ActiveNamedWatch(what.extend(appendName), w);
}
@Override
public long elapsed() {
return w.elapsed(TimeUnit.MICROSECONDS);
}
}
@Data
class PassiveNamedWatch implements NamedWatch {
@Override
public QueryTrace end() {
return PASSIVE;
}
@Override
public QueryTrace end(final QueryTrace child) {
return PASSIVE;
}
@Override
public QueryTrace end(final List<QueryTrace> children) {
return PASSIVE;
}
@Override
public Joiner joiner() {
return PASSIVE_JOINER;
}
@Override
public NamedWatch watch(final Identifier what) {
return PASSIVE_NAMED_WATCH;
}
@Override
public NamedWatch extendIdentifier(String appendName) {
return PASSIVE_NAMED_WATCH;
}
@Override
public long elapsed() {
return 0L;
}
}
@Data
class Identifier {
private final String name;
@JsonCreator
public Identifier(@JsonProperty("name") String name) {
this.name = name;
}
public Identifier extend(String key) {
return new Identifier(name + "[" + key + "]");
}
@Override
public String toString() {
return name;
}
}
/**
* Joiner multiple query traces into one.
*/
interface Joiner {
/**
* Add a child trace that should be part of the resulting trace.
*
* @param trace Child trace to add
*/
void addChild(QueryTrace trace);
/**
* Create a new trace with a snapshot of the current list of children.
*
* @return a {@link com.spotify.heroic.metric.QueryTrace}
*/
QueryTrace result();
}
/**
* A joiner that joins the given children into a single trace.
*/
@Data
class ActiveJoiner implements Joiner {
private final QueryTrace.NamedWatch watch;
private final ImmutableList.Builder<QueryTrace> children = ImmutableList.builder();
@Override
public void addChild(final QueryTrace trace) {
children.add(trace);
}
@Override
public QueryTrace result() {
return watch.end(children.build());
}
}
/**
* A joiner that does nothing.
*/
class PassiveJoiner implements Joiner {
PassiveJoiner() {
}
@Override
public void addChild(final QueryTrace trace) {
/* do nothing */
}
@Override
public QueryTrace result() {
return PASSIVE;
}
}
class Serializer extends JsonSerializer<QueryTrace> {
@Override
public void serialize(
final QueryTrace value, final JsonGenerator gen, final SerializerProvider serializers
) throws IOException {
if (value instanceof PassiveTrace) {
gen.writeNull();
return;
}
gen.writeObject(((ActiveTrace) value).toModel());
}
}
class Deserializer extends JsonDeserializer<QueryTrace> {
@Override
public QueryTrace getNullValue(final DeserializationContext ctxt)
throws JsonMappingException {
return PASSIVE;
}
@Override
public QueryTrace deserialize(
final JsonParser p, final DeserializationContext ctxt
) throws IOException {
return p.readValueAs(ActiveJsonModel.class).toActive();
}
}
/**
* Intermediate JSON model used for serializing and deserializing active values.
* <p>
* <p>Attempting to immediately serialize something extending
* {@link com.spotify.heroic.metric.QueryTrace} would cause infinite recursion since the same
* serializer would be called over and over. This model breaks that.
*/
@Data
class ActiveJsonModel {
private final Identifier what;
private final long elapsed;
private final List<QueryTrace> children;
/**
* Convert to active instance.
*
* @return a new active instance
*/
QueryTrace toActive() {
return new ActiveTrace(what, elapsed, children);
}
}
}