Merge pull request #152 from DataDog/ark/mongo_quickfix
Fix Mongo Query Sanitizing
This commit is contained in:
commit
ce40cbfbed
|
@ -19,7 +19,7 @@ public class MongoClientInstrumentationTest {
|
||||||
.get(0)
|
.get(0)
|
||||||
.getClass()
|
.getClass()
|
||||||
.getSimpleName())
|
.getSimpleName())
|
||||||
.isEqualTo("TracingCommandListener");
|
.isEqualTo("DDTracingCommandListener");
|
||||||
|
|
||||||
mongoClient.close();
|
mongoClient.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ dependencies {
|
||||||
compileOnly group: 'org.jboss.byteman', name: 'byteman', version: '4.0.0-BETA5'
|
compileOnly group: 'org.jboss.byteman', name: 'byteman', version: '4.0.0-BETA5'
|
||||||
|
|
||||||
compile group: 'io.opentracing.contrib', name: 'opentracing-web-servlet-filter', version: '0.0.9'
|
compile group: 'io.opentracing.contrib', name: 'opentracing-web-servlet-filter', version: '0.0.9'
|
||||||
compile group: 'io.opentracing.contrib', name: 'opentracing-mongo-driver', version: '0.0.3'
|
|
||||||
compile group: 'io.opentracing.contrib', name: 'opentracing-okhttp3', version: '0.0.5'
|
compile group: 'io.opentracing.contrib', name: 'opentracing-okhttp3', version: '0.0.5'
|
||||||
compile group: 'io.opentracing.contrib', name: 'opentracing-jms-common', version: '0.0.3'
|
compile group: 'io.opentracing.contrib', name: 'opentracing-jms-common', version: '0.0.3'
|
||||||
compile group: 'io.opentracing.contrib', name: 'opentracing-jms-2', version: '0.0.3'
|
compile group: 'io.opentracing.contrib', name: 'opentracing-jms-2', version: '0.0.3'
|
||||||
|
|
|
@ -2,14 +2,21 @@ package com.datadoghq.agent.integration;
|
||||||
|
|
||||||
import com.datadoghq.trace.DDTags;
|
import com.datadoghq.trace.DDTags;
|
||||||
import com.mongodb.MongoClientOptions;
|
import com.mongodb.MongoClientOptions;
|
||||||
|
import com.mongodb.event.CommandFailedEvent;
|
||||||
|
import com.mongodb.event.CommandListener;
|
||||||
import com.mongodb.event.CommandStartedEvent;
|
import com.mongodb.event.CommandStartedEvent;
|
||||||
|
import com.mongodb.event.CommandSucceededEvent;
|
||||||
import io.opentracing.Span;
|
import io.opentracing.Span;
|
||||||
import io.opentracing.contrib.mongo.TracingCommandListener;
|
import io.opentracing.Tracer;
|
||||||
import io.opentracing.contrib.mongo.TracingCommandListenerFactory;
|
|
||||||
import io.opentracing.tag.Tags;
|
import io.opentracing.tag.Tags;
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.bson.BsonArray;
|
import org.bson.BsonArray;
|
||||||
import org.bson.BsonDocument;
|
import org.bson.BsonDocument;
|
||||||
|
@ -21,10 +28,6 @@ import org.jboss.byteman.rule.Rule;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class MongoHelper extends DDAgentTracingHelper<MongoClientOptions.Builder> {
|
public class MongoHelper extends DDAgentTracingHelper<MongoClientOptions.Builder> {
|
||||||
|
|
||||||
private static final List<String> WHILDCARD_FIELDS =
|
|
||||||
Arrays.asList("ordered", "insert", "count", "find");
|
|
||||||
private static final BsonValue HIDDEN_CAR = new BsonString("?");
|
|
||||||
|
|
||||||
public MongoHelper(final Rule rule) {
|
public MongoHelper(final Rule rule) {
|
||||||
super(rule);
|
super(rule);
|
||||||
}
|
}
|
||||||
|
@ -42,7 +45,7 @@ public class MongoHelper extends DDAgentTracingHelper<MongoClientOptions.Builder
|
||||||
protected MongoClientOptions.Builder doPatch(final MongoClientOptions.Builder builder)
|
protected MongoClientOptions.Builder doPatch(final MongoClientOptions.Builder builder)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|
||||||
final TracingCommandListener listener = TracingCommandListenerFactory.create(tracer);
|
final DDTracingCommandListener listener = new DDTracingCommandListener(tracer);
|
||||||
builder.addCommandListener(listener);
|
builder.addCommandListener(listener);
|
||||||
|
|
||||||
setState(builder, 1);
|
setState(builder, 1);
|
||||||
|
@ -50,54 +53,129 @@ public class MongoHelper extends DDAgentTracingHelper<MongoClientOptions.Builder
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decorate(final Span span, final CommandStartedEvent event) {
|
public static class DDTracingCommandListener implements CommandListener {
|
||||||
try {
|
/**
|
||||||
// normalize the Mongo command so that parameters are removed from the string
|
* The values of these mongo fields will not be scrubbed out. This allows the non-sensitive
|
||||||
final BsonDocument normalized = norm(event.getCommand());
|
* collection names to be captured.
|
||||||
final String mongoCmd = normalized.toString();
|
*/
|
||||||
|
private static final List<String> UNSCRUBBED_FIELDS =
|
||||||
|
Arrays.asList("ordered", "insert", "count", "find", "create");
|
||||||
|
|
||||||
// add specific resource name and replace the `db.statement` OpenTracing
|
private static final BsonValue HIDDEN_CHAR = new BsonString("?");
|
||||||
// tag with the quantized version of the Mongo command
|
private static final String MONGO_OPERATION = "mongo.query";
|
||||||
span.setTag(DDTags.RESOURCE_NAME, mongoCmd);
|
|
||||||
span.setTag(Tags.DB_STATEMENT.getKey(), mongoCmd);
|
static final String COMPONENT_NAME = "java-mongo";
|
||||||
span.setTag(DDTags.SPAN_TYPE, "mongodb");
|
private final Tracer tracer;
|
||||||
} catch (final Throwable e) {
|
/** Cache for (request id, span) pairs */
|
||||||
log.warn("Couldn't decorate the mongo query: " + e.getMessage(), e);
|
private final Map<Integer, Span> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public DDTracingCommandListener(Tracer tracer) {
|
||||||
|
this.tracer = tracer;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private BsonDocument norm(final BsonDocument origin) {
|
@Override
|
||||||
final BsonDocument normalized = new BsonDocument();
|
public void commandStarted(CommandStartedEvent event) {
|
||||||
for (final Map.Entry<String, BsonValue> entry : origin.entrySet()) {
|
Span span = buildSpan(event);
|
||||||
if (WHILDCARD_FIELDS.contains(entry.getKey())) {
|
cache.put(event.getRequestId(), span);
|
||||||
normalized.put(entry.getKey(), entry.getValue());
|
}
|
||||||
} else {
|
|
||||||
final BsonValue child = norm(entry.getValue());
|
@Override
|
||||||
normalized.put(entry.getKey(), child);
|
public void commandSucceeded(CommandSucceededEvent event) {
|
||||||
|
Span span = cache.remove(event.getRequestId());
|
||||||
|
if (span != null) {
|
||||||
|
span.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BsonValue norm(final BsonArray origin) {
|
@Override
|
||||||
final BsonArray normalized = new BsonArray();
|
public void commandFailed(CommandFailedEvent event) {
|
||||||
for (final BsonValue value : origin) {
|
Span span = cache.remove(event.getRequestId());
|
||||||
final BsonValue child = norm(value);
|
if (span != null) {
|
||||||
normalized.add(child);
|
onError(span, event.getThrowable());
|
||||||
|
span.finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BsonValue norm(final BsonValue origin) {
|
private Span buildSpan(CommandStartedEvent event) {
|
||||||
|
Tracer.SpanBuilder spanBuilder =
|
||||||
|
tracer.buildSpan(MONGO_OPERATION).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT);
|
||||||
|
|
||||||
final BsonValue normalized;
|
Span span = spanBuilder.startManual();
|
||||||
if (origin.isDocument()) {
|
try {
|
||||||
normalized = norm(origin.asDocument());
|
decorate(span, event);
|
||||||
} else if (origin.isArray()) {
|
} catch (final Throwable e) {
|
||||||
normalized = norm(origin.asArray());
|
log.warn("Couldn't decorate the mongo query: " + e.getMessage(), e);
|
||||||
} else {
|
}
|
||||||
normalized = HIDDEN_CAR;
|
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onError(Span span, Throwable throwable) {
|
||||||
|
Tags.ERROR.set(span, Boolean.TRUE);
|
||||||
|
span.log(Collections.singletonMap("error.object", throwable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void decorate(Span span, CommandStartedEvent event) {
|
||||||
|
// scrub the Mongo command so that parameters are removed from the string
|
||||||
|
final BsonDocument scrubbed = scrub(event.getCommand());
|
||||||
|
final String mongoCmd = scrubbed.toString();
|
||||||
|
|
||||||
|
Tags.COMPONENT.set(span, COMPONENT_NAME);
|
||||||
|
Tags.DB_STATEMENT.set(span, mongoCmd);
|
||||||
|
Tags.DB_INSTANCE.set(span, event.getDatabaseName());
|
||||||
|
// add specific resource name
|
||||||
|
span.setTag(DDTags.RESOURCE_NAME, mongoCmd);
|
||||||
|
span.setTag(DDTags.SPAN_TYPE, "mongodb");
|
||||||
|
span.setTag(DDTags.SERVICE_NAME, "mongo");
|
||||||
|
|
||||||
|
Tags.PEER_HOSTNAME.set(span, event.getConnectionDescription().getServerAddress().getHost());
|
||||||
|
|
||||||
|
InetAddress inetAddress =
|
||||||
|
event.getConnectionDescription().getServerAddress().getSocketAddress().getAddress();
|
||||||
|
|
||||||
|
if (inetAddress instanceof Inet4Address) {
|
||||||
|
byte[] address = inetAddress.getAddress();
|
||||||
|
Tags.PEER_HOST_IPV4.set(span, ByteBuffer.wrap(address).getInt());
|
||||||
|
} else {
|
||||||
|
Tags.PEER_HOST_IPV6.set(span, inetAddress.getHostAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
Tags.PEER_PORT.set(span, event.getConnectionDescription().getServerAddress().getPort());
|
||||||
|
Tags.DB_TYPE.set(span, "mongo");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BsonDocument scrub(final BsonDocument origin) {
|
||||||
|
final BsonDocument scrub = new BsonDocument();
|
||||||
|
for (final Map.Entry<String, BsonValue> entry : origin.entrySet()) {
|
||||||
|
if (UNSCRUBBED_FIELDS.contains(entry.getKey()) && entry.getValue().isString()) {
|
||||||
|
scrub.put(entry.getKey(), entry.getValue());
|
||||||
|
} else {
|
||||||
|
final BsonValue child = scrub(entry.getValue());
|
||||||
|
scrub.put(entry.getKey(), child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scrub;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BsonValue scrub(final BsonArray origin) {
|
||||||
|
final BsonArray scrub = new BsonArray();
|
||||||
|
for (final BsonValue value : origin) {
|
||||||
|
final BsonValue child = scrub(value);
|
||||||
|
scrub.add(child);
|
||||||
|
}
|
||||||
|
return scrub;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BsonValue scrub(final BsonValue origin) {
|
||||||
|
final BsonValue scrubbed;
|
||||||
|
if (origin.isDocument()) {
|
||||||
|
scrubbed = scrub(origin.asDocument());
|
||||||
|
} else if (origin.isArray()) {
|
||||||
|
scrubbed = scrub(origin.asArray());
|
||||||
|
} else {
|
||||||
|
scrubbed = HIDDEN_CHAR;
|
||||||
|
}
|
||||||
|
return scrubbed;
|
||||||
}
|
}
|
||||||
return normalized;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
package io.opentracing.contrib.mongo;
|
|
||||||
|
|
||||||
import io.opentracing.Tracer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class exists purely to bypass the reduction in constructor visibility of
|
|
||||||
* TracingCommandListener.
|
|
||||||
*/
|
|
||||||
public class TracingCommandListenerFactory {
|
|
||||||
|
|
||||||
public static TracingCommandListener create(final Tracer tracer) {
|
|
||||||
return new TracingCommandListener(tracer);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -48,15 +48,6 @@ DO
|
||||||
com.datadoghq.agent.InstrumentationRulesManager.registerClassLoad($0);
|
com.datadoghq.agent.InstrumentationRulesManager.registerClassLoad($0);
|
||||||
ENDRULE
|
ENDRULE
|
||||||
|
|
||||||
RULE TracingCommandListener-init
|
|
||||||
CLASS io.opentracing.contrib.mongo.TracingCommandListener
|
|
||||||
METHOD <init>
|
|
||||||
AT EXIT
|
|
||||||
IF TRUE
|
|
||||||
DO
|
|
||||||
com.datadoghq.agent.InstrumentationRulesManager.registerClassLoad($0);
|
|
||||||
ENDRULE
|
|
||||||
|
|
||||||
|
|
||||||
# Instrument OkHttp
|
# Instrument OkHttp
|
||||||
# ===========================
|
# ===========================
|
||||||
|
|
|
@ -70,16 +70,6 @@ DO
|
||||||
patch($0);
|
patch($0);
|
||||||
ENDRULE
|
ENDRULE
|
||||||
|
|
||||||
RULE opentracing-mongo-driver-helper
|
|
||||||
CLASS io.opentracing.contrib.mongo.TracingCommandListener
|
|
||||||
METHOD decorate
|
|
||||||
HELPER com.datadoghq.agent.integration.MongoHelper
|
|
||||||
AT EXIT
|
|
||||||
IF TRUE
|
|
||||||
DO
|
|
||||||
decorate($1, $2);
|
|
||||||
ENDRULE
|
|
||||||
|
|
||||||
|
|
||||||
# Instrument OkHttp
|
# Instrument OkHttp
|
||||||
# ===========================
|
# ===========================
|
||||||
|
|
|
@ -4,23 +4,57 @@ import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||||
|
|
||||||
import com.datadoghq.trace.DDSpan;
|
import com.datadoghq.trace.DDSpan;
|
||||||
import com.datadoghq.trace.DDTracer;
|
import com.datadoghq.trace.DDTracer;
|
||||||
|
import com.mongodb.ServerAddress;
|
||||||
|
import com.mongodb.connection.ClusterId;
|
||||||
|
import com.mongodb.connection.ConnectionDescription;
|
||||||
|
import com.mongodb.connection.ServerId;
|
||||||
import com.mongodb.event.CommandStartedEvent;
|
import com.mongodb.event.CommandStartedEvent;
|
||||||
|
import io.opentracing.tag.Tags;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import org.bson.BsonArray;
|
||||||
import org.bson.BsonDocument;
|
import org.bson.BsonDocument;
|
||||||
|
import org.bson.BsonString;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
public class MongoHelperTest {
|
public class MongoHelperTest {
|
||||||
|
|
||||||
@Test
|
private static ConnectionDescription makeConnection() {
|
||||||
public void test() {
|
return new ConnectionDescription(new ServerId(new ClusterId(), new ServerAddress()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mongoSpan() {
|
||||||
final CommandStartedEvent cmd =
|
final CommandStartedEvent cmd =
|
||||||
new CommandStartedEvent(1, null, "databasename", "query", new BsonDocument());
|
new CommandStartedEvent(1, makeConnection(), "databasename", "query", new BsonDocument());
|
||||||
|
|
||||||
final DDSpan span = new DDTracer().buildSpan("foo").startManual();
|
final DDSpan span = new DDTracer().buildSpan("foo").startManual();
|
||||||
new MongoHelper(null).decorate(span, cmd);
|
MongoHelper.DDTracingCommandListener.decorate(span, cmd);
|
||||||
|
|
||||||
assertThat(span.context().getSpanType()).isEqualTo("mongodb");
|
assertThat(span.context().getSpanType()).isEqualTo("mongodb");
|
||||||
assertThat(span.context().getResourceName())
|
assertThat(span.context().getResourceName())
|
||||||
.isEqualTo(span.context().getTags().get("db.statement"));
|
.isEqualTo(span.context().getTags().get("db.statement"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void queryScrubbing() {
|
||||||
|
// all "secret" strings should be scrubbed out of these queries
|
||||||
|
BsonDocument query1 = new BsonDocument("find", new BsonString("show"));
|
||||||
|
query1.put("stuff", new BsonString("secret"));
|
||||||
|
BsonDocument query2 = new BsonDocument("insert", new BsonString("table"));
|
||||||
|
BsonDocument query2_1 = new BsonDocument("count", new BsonString("show"));
|
||||||
|
query2_1.put("id", new BsonString("secret"));
|
||||||
|
query2.put("docs", new BsonArray(Arrays.asList(new BsonString("secret"), query2_1)));
|
||||||
|
List<BsonDocument> queries = Arrays.asList(query1, query2);
|
||||||
|
for (BsonDocument query : queries) {
|
||||||
|
final CommandStartedEvent cmd =
|
||||||
|
new CommandStartedEvent(1, makeConnection(), "databasename", "query", query);
|
||||||
|
|
||||||
|
final DDSpan span = new DDTracer().buildSpan("foo").startManual();
|
||||||
|
MongoHelper.DDTracingCommandListener.decorate(span, cmd);
|
||||||
|
|
||||||
|
assertThat(span.getTags().get(Tags.DB_STATEMENT.getKey()))
|
||||||
|
.isEqualTo(query.toString().replaceAll("secret", "?"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue