com.launchdarkly.sdk.internal.events.EventOutputFormatter Maven / Gradle / Ivy
package com.launchdarkly.sdk.internal.events;
import com.google.gson.stream.JsonWriter;
import com.launchdarkly.sdk.AttributeRef;
import com.launchdarkly.sdk.EvaluationReason;
import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.internal.events.EventSummarizer.CounterValue;
import com.launchdarkly.sdk.internal.events.EventSummarizer.FlagInfo;
import com.launchdarkly.sdk.internal.events.EventSummarizer.SimpleIntKeyedMap;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import static com.launchdarkly.sdk.internal.GsonHelpers.gsonInstance;
/**
* Transforms analytics events and summary data into the JSON format that we send to LaunchDarkly.
* Rather than creating intermediate objects to represent this schema, we use the Gson streaming
* output API to construct JSON directly.
*
* Test coverage for this logic is in EventOutputTest and DefaultEventProcessorOutputTest. The
* handling of context data and private attribute redaction is implemented in EventContextFormatter
* and tested in more detail in EventContextFormatterTest.
*/
final class EventOutputFormatter {
private final EventContextFormatter contextFormatter;
EventOutputFormatter(EventsConfiguration config) {
this.contextFormatter = new EventContextFormatter(
config.allAttributesPrivate,
config.privateAttributes.toArray(new AttributeRef[config.privateAttributes.size()]));
}
int writeOutputEvents(Event[] events, EventSummarizer.EventSummary summary, Writer writer) throws IOException {
int count = 0;
JsonWriter jsonWriter = new JsonWriter(writer);
jsonWriter.beginArray();
for (Event event: events) {
if (writeOutputEvent(event, jsonWriter)) {
count++;
}
}
if (!summary.isEmpty()) {
writeSummaryEvent(summary, jsonWriter);
count++;
}
jsonWriter.endArray();
jsonWriter.flush();
return count;
}
private boolean writeOutputEvent(Event event, JsonWriter jw) throws IOException {
if (event.getContext() == null || !event.getContext().isValid()) {
// The SDK should never send us an event without a valid context, but if we somehow get one,
// just skip the event since there's no way to serialize it.
return false;
}
if (event instanceof Event.FeatureRequest) {
Event.FeatureRequest fe = (Event.FeatureRequest)event;
jw.beginObject();
writeKindAndCreationDate(jw, fe.isDebug() ? "debug" : "feature", event.getCreationDate());
jw.name("key").value(fe.getKey());
if (fe.isDebug()) {
writeContext(fe.getContext(), jw);
} else {
writeContextKeys(fe.getContext(), jw);
}
if (fe.getVersion() >= 0) {
jw.name("version");
jw.value(fe.getVersion());
}
if (fe.getVariation() >= 0) {
jw.name("variation");
jw.value(fe.getVariation());
}
writeLDValue("value", fe.getValue(), jw);
writeLDValue("default", fe.getDefaultVal(), jw);
if (fe.getPrereqOf() != null) {
jw.name("prereqOf");
jw.value(fe.getPrereqOf());
}
writeEvaluationReason(fe.getReason(), jw);
jw.endObject();
} else if (event instanceof Event.Identify) {
jw.beginObject();
writeKindAndCreationDate(jw, "identify", event.getCreationDate());
writeContext(event.getContext(), jw);
jw.endObject();
} else if (event instanceof Event.Custom) {
Event.Custom ce = (Event.Custom)event;
jw.beginObject();
writeKindAndCreationDate(jw, "custom", event.getCreationDate());
jw.name("key").value(ce.getKey());
writeContextKeys(ce.getContext(), jw);
writeLDValue("data", ce.getData(), jw);
if (ce.getMetricValue() != null) {
jw.name("metricValue");
jw.value(ce.getMetricValue());
}
jw.endObject();
} else if (event instanceof Event.Index) {
jw.beginObject();
writeKindAndCreationDate(jw, "index", event.getCreationDate());
writeContext(event.getContext(), jw);
jw.endObject();
} else if (event instanceof Event.MigrationOp) {
jw.beginObject();
writeKindAndCreationDate(jw, "migration_op", event.getCreationDate());
writeContextKeys(event.getContext(), jw);
Event.MigrationOp me = (Event.MigrationOp)event;
jw.name("operation").value(me.getOperation());
long samplingRatio = me.getSamplingRatio();
if(samplingRatio != 1) {
jw.name("samplingRatio").value(samplingRatio);
}
writeMigrationEvaluation(jw, me);
writeMeasurements(jw, me);
jw.endObject();
} else {
return false;
}
return true;
}
private static void writeMeasurements(JsonWriter jw, Event.MigrationOp me) throws IOException {
jw.name("measurements");
jw.beginArray();
writeInvokedMeasurement(jw, me);
writeConsistencyMeasurement(jw, me);
writeLatencyMeasurement(jw, me);
writeErrorMeasurement(jw, me);
jw.endArray(); // end measurements
}
private static void writeErrorMeasurement(JsonWriter jw, Event.MigrationOp me) throws IOException {
Event.MigrationOp.ErrorMeasurement errorMeasurement = me.getErrorMeasurement();
if(errorMeasurement != null && errorMeasurement.hasMeasurement()) {
jw.beginObject();
jw.name("key").value("error");
jw.name("values");
jw.beginObject();
if(errorMeasurement.hasOldError()) {
jw.name("old").value(errorMeasurement.hasOldError());
}
if(errorMeasurement.hasNewError()) {
jw.name("new").value(errorMeasurement.hasNewError());
}
jw.endObject(); // end of values
jw.endObject(); // end of measurement
}
}
private static void writeLatencyMeasurement(JsonWriter jw, Event.MigrationOp me) throws IOException {
Event.MigrationOp.LatencyMeasurement latencyMeasurement = me.getLatencyMeasurement();
if(latencyMeasurement != null && latencyMeasurement.hasMeasurement()) {
jw.beginObject();
jw.name("key").value("latency_ms");
jw.name("values");
jw.beginObject();
if(latencyMeasurement.getOldLatencyMs() != null) {
jw.name("old").value(latencyMeasurement.getOldLatencyMs());
}
if(latencyMeasurement.getNewLatencyMs() != null) {
jw.name("new").value(latencyMeasurement.getNewLatencyMs());
}
jw.endObject(); // end of values
jw.endObject(); // end of measurement
}
}
private static void writeConsistencyMeasurement(JsonWriter jw, Event.MigrationOp me) throws IOException {
Event.MigrationOp.ConsistencyMeasurement consistencyMeasurement = me.getConsistencyMeasurement();
if(consistencyMeasurement != null) {
jw.beginObject();
jw.name("key").value("consistent");
jw.name("value").value(consistencyMeasurement.isConsistent());
if(consistencyMeasurement.getSamplingRatio() != 1) {
jw.name("samplingRatio").value(consistencyMeasurement.getSamplingRatio());
}
jw.endObject(); // end measurement
}
}
private static void writeInvokedMeasurement(JsonWriter jw, Event.MigrationOp me) throws IOException {
jw.beginObject();
jw.name("key").value("invoked");
Event.MigrationOp.InvokedMeasurement invokedMeasurement = me.getInvokedMeasurement();
jw.name("values");
jw.beginObject();
if(invokedMeasurement.wasOldInvoked()) {
jw.name("old").value(invokedMeasurement.wasOldInvoked());
}
if(invokedMeasurement.wasNewInvoked()) {
jw.name("new").value(invokedMeasurement.wasNewInvoked());
}
jw.endObject(); // end values
jw.endObject(); // end measurement
}
private void writeMigrationEvaluation(JsonWriter jw, Event.MigrationOp me) throws IOException {
jw.name("evaluation");
jw.beginObject();
jw.name("key").value(me.getFeatureKey());
if (me.getVariation() >= 0) {
jw.name("variation");
jw.value(me.getVariation());
}
if (me.getFlagVersion() >= 0) {
jw.name("version");
jw.value(me.getFlagVersion());
}
writeLDValue("value", me.getValue(), jw);
writeLDValue("default", me.getDefaultVal(), jw);
writeEvaluationReason(me.getReason(), jw);
jw.endObject();
}
private void writeSummaryEvent(EventSummarizer.EventSummary summary, JsonWriter jw) throws IOException {
jw.beginObject();
jw.name("kind");
jw.value("summary");
jw.name("startDate");
jw.value(summary.startDate);
jw.name("endDate");
jw.value(summary.endDate);
jw.name("features");
jw.beginObject();
for (Map.Entry flag: summary.counters.entrySet()) {
String flagKey = flag.getKey();
FlagInfo flagInfo = flag.getValue();
jw.name(flagKey);
jw.beginObject();
writeLDValue("default", flagInfo.defaultVal, jw);
jw.name("contextKinds").beginArray();
for (String kind: flagInfo.contextKinds) {
jw.value(kind);
}
jw.endArray();
jw.name("counters");
jw.beginArray();
for (int i = 0; i < flagInfo.versionsAndVariations.size(); i++) {
int version = flagInfo.versionsAndVariations.keyAt(i);
SimpleIntKeyedMap variations = flagInfo.versionsAndVariations.valueAt(i);
for (int j = 0; j < variations.size(); j++) {
int variation = variations.keyAt(j);
CounterValue counter = variations.valueAt(j);
jw.beginObject();
if (variation >= 0) {
jw.name("variation").value(variation);
}
if (version >= 0) {
jw.name("version").value(version);
} else {
jw.name("unknown").value(true);
}
writeLDValue("value", counter.flagValue, jw);
jw.name("count").value(counter.count);
jw.endObject();
}
}
jw.endArray(); // end of "counters" array
jw.endObject(); // end of this flag
}
jw.endObject(); // end of "features"
jw.endObject(); // end of summary event object
}
private void writeKindAndCreationDate(JsonWriter jw, String kind, long creationDate) throws IOException {
jw.name("kind").value(kind);
jw.name("creationDate").value(creationDate);
}
private void writeContext(LDContext context, JsonWriter jw) throws IOException {
jw.name("context");
contextFormatter.write(context, jw);
}
private void writeContextKeys(LDContext context, JsonWriter jw) throws IOException {
jw.name("contextKeys").beginObject();
for (int i = 0; i < context.getIndividualContextCount(); i++) {
LDContext c = context.getIndividualContext(i);
if (c != null) {
jw.name(c.getKind().toString()).value(c.getKey());
}
}
jw.endObject();
}
private void writeLDValue(String key, LDValue value, JsonWriter jw) throws IOException {
if (value == null || value.isNull()) {
return;
}
jw.name(key);
gsonInstance().toJson(value, LDValue.class, jw); // LDValue defines its own custom serializer
}
private void writeEvaluationReason(EvaluationReason er, JsonWriter jw) throws IOException {
if (er == null) {
return;
}
jw.name("reason");
gsonInstance().toJson(er, EvaluationReason.class, jw); // EvaluationReason defines its own custom serializer
}
}