Skip to content
4 changes: 2 additions & 2 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ endif::[]

* Add support for <<supported-databases, Redis Lettuce client>>
* Add `context.message.age.ms` field for JMS message receiving spans and transactions - {pull}970[#970]
* Instrument log4j Logger#error(String, Throwable) ({pull}919[#919])
Automatically captures exceptions when calling `logger.error("message", exception)`
* Instrument log4j Logger#error(String, Throwable) ({pull}919[#919]) Automatically captures exceptions when calling `logger.error("message", exception)`
* Add instrumentation for external process execution through `java.lang.Process` and Apache `commons-exec` - {pull}903[#903]
* Add `destination` fields to exit span contexts - {pull}976[#976]

[float]
===== Bug Fixes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 - 2019 Elastic and contributors
* %%
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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.
* #L%
*/
package co.elastic.apm.agent.impl.context;

import co.elastic.apm.agent.objectpool.Recyclable;

import javax.annotation.Nullable;

/**
* Context information about a destination of outgoing calls.
*/
public class Destination implements Recyclable {

/**
* An IP (v4 or v6) or a host/domain name.
*/
private final StringBuilder address = new StringBuilder();

/**
* The destination's port used within this context.
*/
private int port;

public Destination withAddress(@Nullable CharSequence address) {
if (address != null && address.length() > 0) {
// remove square brackets for IPv6 addresses
int startIndex = 0;
if (address.charAt(0) == '[') {
startIndex = 1;
}
int endIndex = address.length();
if (address.charAt(endIndex - 1) == ']') {
endIndex--;
}
this.address.append(address, startIndex, endIndex);
}
return this;
}

public StringBuilder getAddress() {
return address;
}

public Destination withPort(int port) {
this.port = port;
return this;
}

public int getPort() {
return port;
}

/**
* Information about the service related to this destination.
*/
private final Service service = new Service();

public Service getService() {
return service;
}

public boolean hasContent() {
return address.length() > 0 || port > 0 || service.hasContent();
}

@Override
public void resetState() {
address.setLength(0);
port = -1;
service.resetState();
}

/**
* Context information required for service maps.
*/
public static class Service implements Recyclable {
/**
* Used for detecting unique destinations from each service.
* For HTTP, this is the address, with the port (even when it's the default port), without any scheme.
* For other types of connections, it's just the {@code span.subtype} (kafka, elasticsearch etc.).
* For messaging, we additionally add the queue name (eg jms/myQueue).
*/
private final StringBuilder resource = new StringBuilder();

/**
* Used for detecting “sameness” of services and then the display name of a service in the Service Map.
* In other words, the {@link Service#resource} is used to query data for ALL destinations. However,
* some `resources` may be nodes of the same cluster, in which case we also want to be aware.
* Eventually, we may decide to actively fetch a cluster name or similar and we could use that to detect "sameness".
* For now, for HTTP we use scheme, host, and non-default port. For anything else, we use {@code span.subtype}
* (for example- postgresql, elasticsearch).
*/
private final StringBuilder name = new StringBuilder();

/**
* For displaying icons or similar. Currently, this should be equal to the {@code span.type}.
*/
@Nullable
private String type;

public Service withResource(String resource) {
this.resource.append(resource);
return this;
}

public StringBuilder getResource() {
return resource;
}

public Service withName(String name) {
this.name.append(name);
return this;
}

public StringBuilder getName() {
return name;
}

public Service withType(String type) {
this.type = type;
return this;
}

@Nullable
public String getType() {
return type;
}

public boolean hasContent() {
return resource.length() > 0 || name.length() > 0 || type != null;
}

@Override
public void resetState() {
resource.setLength(0);
name.setLength(0);
type = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
* 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
Expand Down Expand Up @@ -41,6 +41,11 @@ public class SpanContext extends AbstractContext {
*/
private final Http http = new Http();

/**
* An object containing contextual data for service maps
*/
private final Destination destination = new Destination();

/**
* An object containing contextual data for database spans
*/
Expand All @@ -55,14 +60,22 @@ public Http getHttp() {
return http;
}

/**
* An object containing contextual data for service maps
*/
public Destination getDestination() {
return destination;
}

@Override
public void resetState() {
super.resetState();
db.resetState();
http.resetState();
destination.resetState();
}

public boolean hasContent() {
return super.hasContent() || db.hasContent() || http.hasContent();
return super.hasContent() || db.hasContent() || http.hasContent() || destination.hasContent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,16 @@ public String getNameAsString() {
* as the underlying {@link StringBuilder} instance will be reused.
* </p>
*
* @param s the string to append to the name
* @param cs the char sequence to append to the name
* @return {@code this}, for chaining
*/
public T appendToName(String s) {
return appendToName(s, PRIO_DEFAULT);
public T appendToName(CharSequence cs) {
return appendToName(cs, PRIO_DEFAULT);
}

public T appendToName(String s, int priority) {
public T appendToName(CharSequence cs, int priority) {
if (priority >= namePriority) {
this.name.append(s);
this.name.append(cs);
this.namePriority = priority;
}
return (T) this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import co.elastic.apm.agent.impl.MetaData;
import co.elastic.apm.agent.impl.context.AbstractContext;
import co.elastic.apm.agent.impl.context.Destination;
import co.elastic.apm.agent.impl.context.Db;
import co.elastic.apm.agent.impl.context.Http;
import co.elastic.apm.agent.impl.context.Message;
Expand Down Expand Up @@ -729,6 +730,7 @@ private void serializeSpanContext(SpanContext context, TraceContext traceContext
serializeMessageContext(context.getMessage());
serializeDbContext(context.getDb());
serializeHttpContext(context.getHttp());
serializeDestination(context.getDestination());

writeFieldName("tags");
serializeLabels(context);
Expand All @@ -737,6 +739,33 @@ private void serializeSpanContext(SpanContext context, TraceContext traceContext
jw.writeByte(COMMA);
}

private void serializeDestination(Destination destination) {
if (destination.hasContent()) {
writeFieldName("destination");
jw.writeByte(OBJECT_START);
if (destination.getAddress().length() > 0) {
writeField("address", destination.getAddress());
}
if (destination.getPort() > 0) {
writeField("port", destination.getPort());
}
serializeService(destination.getService());
jw.writeByte(OBJECT_END);
jw.writeByte(COMMA);
}
}

private void serializeService(Destination.Service service) {
if (service.hasContent()) {
writeFieldName("service");
jw.writeByte(OBJECT_START);
writeField("name", service.getName());
writeField("resource", service.getResource());
writeLastField("type", service.getType());
jw.writeByte(OBJECT_END);
}
}

private void serializeMessageContext(final Message message) {
if (message.hasContent()) {
writeFieldName("message");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
*/
package co.elastic.apm.agent;

import co.elastic.apm.agent.impl.context.Destination;
import co.elastic.apm.agent.impl.error.ErrorCapture;
import co.elastic.apm.agent.impl.error.ErrorPayload;
import co.elastic.apm.agent.impl.payload.PayloadUtils;
Expand Down Expand Up @@ -64,6 +65,12 @@ public class MockReporter implements Reporter {
private static final JsonSchema errorSchema;
private static final JsonSchema spanSchema;
private static final DslJsonSerializer dslJsonSerializer;

// A set of exit span subtypes that do not support address and port discovery
private static final Set<String> SPAN_TYPES_WITHOUT_ADDRESS;
// And for any case the disablement of the check cannot rely on subtype (eg Redis, where Jedis supports and Lettuce does not)
private boolean disableDestinationAddressCheck;

private final List<Transaction> transactions = new ArrayList<>();
private final List<Span> spans = new ArrayList<>();
private final List<ErrorCapture> errors = new ArrayList<>();
Expand All @@ -78,6 +85,7 @@ public class MockReporter implements Reporter {
ApmServerClient apmServerClient = mock(ApmServerClient.class);
when(apmServerClient.isAtLeast(any())).thenReturn(true);
dslJsonSerializer = new DslJsonSerializer(mock(StacktraceConfiguration.class), apmServerClient);
SPAN_TYPES_WITHOUT_ADDRESS = Set.of("jms");
}

public MockReporter() {
Expand All @@ -93,6 +101,10 @@ private static JsonSchema getSchema(String resource) {
return JsonSchemaFactory.getInstance().getSchema(MockReporter.class.getResourceAsStream(resource));
}

public void disableDestinationAddressCheck() {
disableDestinationAddressCheck = true;
}

@Override
public synchronized void report(Transaction transaction) {
if (closed) {
Expand All @@ -108,9 +120,25 @@ public synchronized void report(Span span) {
return;
}
verifySpanSchema(asJson(dslJsonSerializer.toJsonString(span)));
verifyDestinationFields(span);
spans.add(span);
}

private void verifyDestinationFields(Span span) {
if (!span.isExit()) {
return;
}
Destination destination = span.getContext().getDestination();
if (!disableDestinationAddressCheck && !SPAN_TYPES_WITHOUT_ADDRESS.contains(span.getSubtype())) {
assertThat(destination.getAddress()).isNotEmpty();
assertThat(destination.getPort()).isGreaterThan(0);
}
Destination.Service service = destination.getService();
assertThat(service.getName()).isNotEmpty();
assertThat(service.getResource()).isNotEmpty();
assertThat(service.getType()).isNotNull();
}

public void verifyTransactionSchema(JsonNode jsonNode) {
verifyJsonSchema(transactionSchema, jsonNode);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,28 @@ void testSpanHttpContextSerialization() {
assertThat(http.get("status_code").intValue()).isEqualTo(523);
}

@Test
void testSpanDestinationContextSerialization() {
Span span = new Span(MockTracer.create());
span.getContext().getDestination().withAddress("whatever.com").withPort(80)
.getService()
.withName("http://whatever.com")
.withResource("whatever.com:80")
.withType("external");

JsonNode spanJson = readJsonString(serializer.toJsonString(span));
JsonNode context = spanJson.get("context");
JsonNode destination = context.get("destination");
assertThat(destination).isNotNull();
assertThat("whatever.com").isEqualTo(destination.get("address").textValue());
assertThat(80).isEqualTo(destination.get("port").intValue());
JsonNode service = destination.get("service");
assertThat(service).isNotNull();
assertThat("http://whatever.com").isEqualTo(service.get("name").textValue());
assertThat("whatever.com:80").isEqualTo(service.get("resource").textValue());
assertThat("external").isEqualTo(service.get("type").textValue());
}

@Test
void testSpanMessageContextSerialization() {
Span span = new Span(MockTracer.create());
Expand Down
Loading