Skip to content

Commit

Permalink
#23 pluggable logging with basic impl
Browse files Browse the repository at this point in the history
  • Loading branch information
eric239 committed Jun 1, 2016
1 parent dc0f48f commit 3b2ea4f
Show file tree
Hide file tree
Showing 11 changed files with 431 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import java.util.Collections;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;

import com.glassdoor.planout4j.config.Planout4jRepository;
import com.glassdoor.planout4j.config.Planout4jRepositoryImpl;
import com.glassdoor.planout4j.config.ValidationException;
import com.glassdoor.planout4j.logging.LogRecord;
import com.glassdoor.planout4j.logging.Planout4jLogger;
import com.glassdoor.planout4j.logging.Planout4jLoggerFactory;
import com.glassdoor.planout4j.util.VersionLogger;
import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.base.Preconditions.*;
import static com.google.common.base.Preconditions.checkNotNull;

/**
* Reads namespace data once and caches forever.
Expand All @@ -26,16 +29,18 @@ public class SimpleNamespaceFactory implements NamespaceFactory {
private static final Logger LOG = LoggerFactory.getLogger(SimpleNamespaceFactory.class);

protected final Planout4jRepository planout4jRepository;
protected final Planout4jLogger p4jLogger;

protected volatile Map<String, NamespaceConfig> namespaceName2namespaceConfigMap = Collections.emptyMap();

public SimpleNamespaceFactory(final Planout4jRepository planout4jRepository) {
public SimpleNamespaceFactory(final Planout4jRepository planout4jRepository, final Planout4jLogger p4jLogger) {
this.planout4jRepository = planout4jRepository;
this.p4jLogger = MoreObjects.firstNonNull(p4jLogger, Planout4jLogger.NO_OP);
namespaceName2namespaceConfigMap = readConfig();
}

public SimpleNamespaceFactory() {
this(new Planout4jRepositoryImpl());
this(new Planout4jRepositoryImpl(), Planout4jLoggerFactory.getLogger());
}

/**
Expand All @@ -49,8 +54,18 @@ public SimpleNamespaceFactory() {
@Override
public Optional<Namespace> getNamespace(final String name, final Map<String, ?> paramName2valueMap, final Map<String, ?> overrides) {
final Optional<NamespaceConfig> config = getNamespaceConfig(name);
return config.isPresent() ? Optional.of(new Namespace(config.get(), paramName2valueMap, overrides))
: Optional.<Namespace>absent();
final Namespace ns;
if (config.isPresent()) {
ns = new Namespace(config.get(), paramName2valueMap, overrides);
try {
p4jLogger.exposed(new LogRecord(ns, paramName2valueMap, overrides));
} catch (final RuntimeException e) {
LOG.warn("Failure in exposure logging", e);
}
} else {
ns = null;
}
return Optional.fromNullable(ns);
}

/**
Expand Down Expand Up @@ -79,7 +94,7 @@ public void refresh() {
LOG.info("refreshing ...");
try {
namespaceName2namespaceConfigMap = readConfig();
} catch (Exception e) {
} catch (final Exception e) {
LOG.error("Namespace refresh failed: Invalid configuration", e);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.glassdoor.planout4j.logging;

public abstract class AbstractJSONLogger extends AbstractSerializingLogger<String> {

protected AbstractJSONLogger() {
this(false);
}

protected AbstractJSONLogger(final boolean pretty) {
super(new JSONSerializer(pretty));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.glassdoor.planout4j.logging;

import java.util.Objects;

import com.google.common.base.Converter;
import com.typesafe.config.Config;


/**
* Base class for all serializing loggers.
* @param <S> what to serialize to
* @author ernest_mishkin
*/
public abstract class AbstractSerializingLogger<S> implements Planout4jLogger {

private static final String LOG_DEFAULT_EXPOSURE = "log_default_exposure";

protected final Converter<LogRecord, S> serializer;
public volatile boolean logDefaultExposure;

protected AbstractSerializingLogger(final Converter<LogRecord, S> serializer) {
this.serializer = Objects.requireNonNull(serializer);
}

/**
* Set certain properties from the configuration (<code>logging</code> section of the master planout4j.conf file)
* @param config logging section config
*/
@Override
public void configure(final Config config) {
logDefaultExposure = config.hasPath(LOG_DEFAULT_EXPOSURE) && config.getBoolean(LOG_DEFAULT_EXPOSURE);
}

/**
* How to persist the serialized record
* @param record serialized record
*/
protected abstract void persist(S record);

@Override
public void exposed(final LogRecord record) {
if (logDefaultExposure || record.namespace.getExperiment() != null) {
persist(serializer.convert(record));
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.glassdoor.planout4j.logging;

import java.util.Map;


@SuppressWarnings("unused")
class JSONLoggingRecord {

String event;
String timestamp;
String namespace;
String experiment;
String salt;
String checksum;
Map inputs;
Map overrides;
Map params;

public String getEvent() {
return event;
}

public String getTimestamp() {
return timestamp;
}

public String getNamespace() {
return namespace;
}

public String getExperiment() {
return experiment;
}

public String getSalt() {
return salt;
}

public String getChecksum() {
return checksum;
}

public Map getInputs() {
return inputs;
}

public Map getOverrides() {
return overrides;
}

public Map getParams() {
return params;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.glassdoor.planout4j.logging;


import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

import com.glassdoor.planout4j.Experiment;
import com.google.common.base.Converter;
import com.google.common.base.MoreObjects;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;


/**
* Serializes an exposure logging record into JSON string.
* Format of the record follows <a href="https://facebook.github.io/planout/docs/logging.html">planout example</a> with minor tweaks.
* Sample record:<pre>
{
"event": "exposure",
"timestamp": "2016-05-31T19:06:58.510Z",
"namespace": "test_ns",
"experiment": "def_exp",
"checksum": "77341e05",
"inputs": {
"userid": 12345
},
"overrides": {
},
"params": {
"specific_goal": true,
"group_size": 1,
"ratings_per_user_goal": 64,
"ratings_goal": 64
}
}
* </pre>
* @author ernest_mishkin
*/
public class JSONSerializer extends Converter<LogRecord, String> {

private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
private static final DateFormat ISO = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
static {
ISO.setTimeZone(UTC);
}
private static final HashFunction CHECKSUM = Hashing.crc32();

private final Gson gson;

public JSONSerializer() {
this(false);
}

public JSONSerializer(final boolean pretty) {
final GsonBuilder builder = new GsonBuilder();
if (pretty) {
builder.setPrettyPrinting();
}
gson = builder.create();
}

@Override
protected String doForward(final LogRecord record) {
final JSONLoggingRecord jlr = new JSONLoggingRecord();
jlr.event = "exposure";
jlr.timestamp = ISO.format(new Date());
jlr.namespace = record.namespace.getName();
jlr.salt = record.namespace.nsConf.salt;
final Experiment exp = MoreObjects.firstNonNull(record.namespace.getExperiment(), record.namespace.nsConf.getDefaultExperiment());
jlr.experiment = exp.name;
jlr.checksum = CHECKSUM.hashUnencodedChars(exp.def.getCopyOfScript().toString()).toString();
jlr.inputs = record.input;
jlr.overrides = record.overrides;
jlr.params = record.namespace.getParams();
return gson.toJson(jlr);
}

@Override
protected LogRecord doBackward(final String s) {
throw new IllegalStateException("Deserialization is not implemented");
}


}
60 changes: 60 additions & 0 deletions api/src/main/java/com/glassdoor/planout4j/logging/LogRecord.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.glassdoor.planout4j.logging;

import java.util.Collections;
import java.util.Map;
import java.util.Objects;

import com.glassdoor.planout4j.Namespace;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;


/**
* Represents data logged at exposure.
* @author ernest_mishkin
*/
public class LogRecord {

public final Namespace namespace;
public final Map<String, ?> input;
public final Map<String, ?> overrides;

public LogRecord(final Namespace namespace, final Map<String, ?> input, final Map<String, ?> overrides) {
this.namespace = namespace;
this.input = ImmutableMap.copyOf(input);
//noinspection unchecked,CollectionsFieldAccessReplaceableByMethodCall
this.overrides = overrides == null ? Collections.EMPTY_MAP : ImmutableMap.copyOf(overrides);
}


@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final LogRecord record = (LogRecord) o;
return Objects.equals(namespace, record.namespace) &&
Objects.equals(input, record.input) &&
Objects.equals(overrides, record.overrides);
}

@Override
public int hashCode() {
return Objects.hash(namespace, input, overrides);
}


@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("namespace", namespace.getName())
.add("experiment", namespace.getExperiment().name)
.add("input", input)
.add("overrides", overrides)
.toString();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.glassdoor.planout4j.logging;

import com.typesafe.config.Config;


/**
* Implementations of this interface are responsible for performing exposure logging.
* If the logging involves more than trivial amount of time/resources, it should be done in a separate thread
* as planout4j code itself will perform logging serially.
* @author ernest_mishkin
*/
public interface Planout4jLogger {

/**
* Invoked when input (e.g. a user) has been exposed to an experiment within a specific namespace.
* Any runtime exceptions thrown within implementations will be caught and logged (but will <b>not</b> break the flow).
* @param record exposure details
*/
void exposed(LogRecord record);

/**
* Set certain properties from the configuration (<code>logging</code> section of the master planout4j.conf file)
* @param config logging section config
*/
void configure(Config config);


/**
* The "no-op" logger, does nothing. Default one for backwards-compatibility reasons.
*/
Planout4jLogger NO_OP = new Planout4jLogger() {
@Override
public void exposed(final LogRecord record) {}
@Override
public void configure(final Config config) {}
};


}
Loading

0 comments on commit 3b2ea4f

Please sign in to comment.