Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ for exit spans - {pull}1788[#1788]
* Propagate trace context headers in HTTP calls occurring from within traced exit points, for example - when using
Elasticsearch's REST client - {pull}1883[#1883]
* Added support for naming sparkjava (not Apache Spark) transactions {pull}1894[#1894]
* Added the ability to manually create exit spans, which will result with the auto creation of service nodes in the
service map and downstream service in the dependencies table - {pull}1898[#1898]

[float]
===== Bug fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ public Span startSpan(String type, @Nullable String subtype, @Nullable String ac
return NoopSpan.INSTANCE;
}

@Nonnull
@Override
public Span startExitSpan(String type, String subtype, @Nullable String action) {
Object span = doCreateExitSpan();
if (span != null) {
doSetTypes(span, type, subtype, action);
return new SpanImpl(span);
}
return NoopSpan.INSTANCE;
}

@Nonnull
@Override
public Span startSpan() {
Expand All @@ -74,6 +85,11 @@ private Object doCreateSpan() {
return null;
}

private Object doCreateExitSpan() {
// co.elastic.apm.agent.pluginapi.AbstractSpanInstrumentation$DoCreateExitSpanInstrumentation.doCreateExitSpan
return null;
}

void doSetName(String name) {
// co.elastic.apm.agent.pluginapi.AbstractSpanInstrumentation$SetNameInstrumentation.doSetName
}
Expand Down
6 changes: 6 additions & 0 deletions apm-agent-api/src/main/java/co/elastic/apm/api/NoopSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ public Span startSpan(String type, @Nullable String subtype, @Nullable String ac
return INSTANCE;
}

@Nonnull
@Override
public Span startExitSpan(String type, String subtype, @Nullable String action) {
return INSTANCE;
}

@Nonnull
@Override
public Span startSpan() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ public Span startSpan(String type, @Nullable String subtype, @Nullable String ac
return NoopSpan.INSTANCE;
}

@Nonnull
@Override
public Span startExitSpan(String type, String subtype, @Nullable String action) {
return NoopSpan.INSTANCE;
}

@Nonnull
@Override
public Span startSpan() {
Expand Down
18 changes: 18 additions & 0 deletions apm-agent-api/src/main/java/co/elastic/apm/api/Span.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,24 @@ public interface Span {
@Nonnull
Span startSpan(String type, @Nullable String subtype, @Nullable String action);

/**
* Start and return a new typed custom exit span as a child of this span.
* <p>
* Similar to {@link #startSpan(String, String, String)}, but the created span will be used to create a node in
* the Service Map and a downstream service in the Dependencies Table. The provided subtype will be used as the
* downstream service name, unless the {@code destination.service.resource} field is explicitly set through
* {@link #setDestinationService(String)}.
* <p>
* If invoked on a span which is already an exit span, this method will return a noop span.
*
* @param type The type of the create span. If a known type is provide, it may be used to select service icon
* @param subtype The subtype of the created span. Will be used as the downstream service name. Cannot be {@code null}
* @param action The action related to the new span
* @return the started exit span, or a noop span if this span is already an exit span, never {@code null}
*/
@Nonnull
Span startExitSpan(String type, String subtype, @Nullable String action);

/**
* Start and return a new custom span with no type, as a child of this span.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ private void setResourceValue(String newValue) {
resource.append(newValue);
}

public boolean isResourceSetByUser() {
return resourceSetByUser;
}

public StringBuilder getResource() {
return resource;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@

import co.elastic.apm.agent.configuration.CoreConfiguration;
import co.elastic.apm.agent.impl.ElasticApmTracer;
import co.elastic.apm.agent.impl.context.Db;
import co.elastic.apm.agent.impl.context.Destination;
import co.elastic.apm.agent.impl.context.Message;
import co.elastic.apm.agent.impl.context.SpanContext;
import co.elastic.apm.agent.impl.context.Url;
import co.elastic.apm.agent.impl.context.web.ResultUtil;
import co.elastic.apm.agent.objectpool.Recyclable;
import org.slf4j.Logger;
Expand Down Expand Up @@ -248,6 +252,35 @@ public void beforeEnd(long epochMicros) {
withOutcome(outcome);
}

// auto-infer context.destination.service.resource as per spec:
// https://github.com/elastic/apm/blob/master/specs/agents/tracing-spans-destination.md#contextdestinationserviceresource
Destination.Service service = getContext().getDestination().getService();
StringBuilder serviceResource = service.getResource();
if (isExit() && serviceResource.length() == 0 && !service.isResourceSetByUser()) {
String resourceType = (subtype != null) ? subtype : type;
Db db = context.getDb();
Message message = context.getMessage();
Url internalUrl = context.getHttp().getInternalUrl();
if (db.hasContent()) {
serviceResource.append(resourceType);
if (db.getInstance() != null) {
serviceResource.append('/').append(db.getInstance());
}
} else if (message.hasContent()) {
serviceResource.append(resourceType);
if (message.getQueueName() != null) {
serviceResource.append('/').append(message.getQueueName());
}
} else if (internalUrl.hasContent()) {
serviceResource.append(internalUrl.getHostname());
if (internalUrl.getPort() > 0) {
serviceResource.append(':').append(internalUrl.getPort());
}
} else {
serviceResource.append(resourceType);
}
}

if (transaction != null) {
transaction.incrementTimer(type, subtype, getSelfDuration());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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.
*/
package co.elastic.apm.agent.impl.context;

import co.elastic.apm.agent.MockTracer;
import co.elastic.apm.agent.impl.transaction.Span;
import co.elastic.apm.agent.impl.transaction.Transaction;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import specs.TestJsonSpec;

import javax.annotation.Nullable;
import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static org.assertj.core.api.Assertions.assertThat;

public class ServiceResourceTest {

private static Transaction root;

@BeforeAll
static void startRootTransaction() {
root = Objects.requireNonNull(MockTracer.createRealTracer().startRootTransaction(null));
}

@AfterAll
static void endTransaction() {
root.end();
}

@ParameterizedTest
@MethodSource("getTestCases")
void testServiceResourceInference(JsonNode testCase) {
Span span = createSpan(testCase);
StringBuilder serviceResource = span.getContext().getDestination().getService().getResource();
// auto-inference happens now
span.end();
String expected = getTextValueOrNull(testCase, "expected_resource");
if (expected == null) {
expected = "";
}
String actual = serviceResource.toString();
assertThat(actual)
.withFailMessage(String.format("%s, expected: `%s`, actual: `%s`", getTextValueOrNull(testCase, "failure_message"), expected, actual))
.isEqualTo(expected);
}

private Span createSpan(JsonNode testCase) {
Span span = root.createSpan();
JsonNode spanJson = testCase.get("span");
span.withType(spanJson.get("type").textValue());
JsonNode subtypeJsonNode = spanJson.get("subtype");
if (subtypeJsonNode != null) {
span.withSubtype(subtypeJsonNode.textValue());
}
if (spanJson.get("exit").asBoolean(false)) {
span.asExit();
}
JsonNode contextJson = spanJson.get("context");
if (contextJson != null) {
SpanContext context = span.getContext();
JsonNode dbJson = contextJson.get("db");
if (dbJson != null) {
Db db = context.getDb();
db.withType(getTextValueOrNull(dbJson, "type"));
db.withInstance(getTextValueOrNull(dbJson, "instance"));
}
JsonNode messageJson = contextJson.get("message");
if (messageJson != null) {
Message message = context.getMessage();
message.withBody(getTextValueOrNull(messageJson, "body"));
JsonNode queueJson = messageJson.get("queue");
if (queueJson != null) {
message.withQueue(queueJson.get("name").asText());
}
}
JsonNode httpJson = contextJson.get("http");
if (httpJson != null) {
JsonNode urlJson = httpJson.get("url");
if (urlJson != null) {
Url url = context.getHttp().getInternalUrl();
url.withHostname(getTextValueOrNull(urlJson, "host"));
JsonNode portJson = urlJson.get("port");
if (portJson != null) {
url.withPort(portJson.intValue());
}
}
}
JsonNode destinationJson = contextJson.get("destination");
if (destinationJson != null) {
JsonNode serviceJson = destinationJson.get("service");
if (serviceJson != null) {
String resource = getTextValueOrNull(serviceJson, "resource");
if (resource != null) {
context.getDestination().getService().withResource(resource);
}
}
}
}
return span;
}

@Nullable
private String getTextValueOrNull(JsonNode dbJson, String type) {
JsonNode jsonNode = dbJson.get(type);
if (jsonNode == null || jsonNode.isNull()) {
return null;
}
return jsonNode.asText();
}

private static Stream<JsonNode> getTestCases() {
Iterator<JsonNode> json = TestJsonSpec.getJson("service_resource_inference.json").iterator();
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(json, Spliterator.ORDERED), false);
}
}
Loading