Skip to content

Commit

Permalink
Auto-inference of destination.service.resource (#1898)
Browse files Browse the repository at this point in the history
  • Loading branch information
eyalkoren authored Jul 7, 2021
1 parent 0c39ed8 commit d0e00ba
Show file tree
Hide file tree
Showing 13 changed files with 533 additions and 2 deletions.
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

0 comments on commit d0e00ba

Please sign in to comment.