Trace delivers best practices in a multi-threaded asynchronous system

Common implementation methods of JAVA thread asynchronous are:

  • new Thread
  • ExecutorService

Of course, there are others, such as fork-join, which will be mentioned below. The following mainly focuses on these two scenarios for practice with DDTrace and Springboot.

Introducing DDTrace sdk

    <properties>
        <java.version>1.8</java.version>
        <dd.version>1.21.0</dd.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.datadoghq</groupId>
            <artifactId>dd-trace-api</artifactId>
            <version>${dd.version}</version>
        </dependency>

        <dependency>
            <groupId>io.opentracing</groupId>
            <artifactId>opentracing-api</artifactId>
            <version>0.33.0</version>
        </dependency>
        <dependency>
            <groupId>io.opentracing</groupId>
            <artifactId>opentracing-mock</artifactId>
            <version>0.33.0</version>
        </dependency>
        <dependency>
            <groupId>io.opentracing</groupId>
            <artifactId>opentracing-util</artifactId>
            <version>0.33.0</version>
        </dependency>
    ...

About the usage of DDTrace sdk reference document ddtrace-api usage guide

Logback configuration

Configure logback to output traceId and spanId, and apply the following pattern to all appender in.

<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - [%method,%line] %X {dd.service} %X{dd.trace_id} %X{dd.span_id} - %msg%n" />

If link information is generated, Trace information will be output in the log.

new Thread

Implement a simple interface, use logback to output log information, and observe the log output.

 @RequestMapping("/thread")
    @ResponseBody
    public String threadTest(){<!-- -->
        logger.info("this func is threadTest.");
        return "success";
    }

After the request, the log is as follows

2023-10-23 11:33:09.983 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.CalcFilter - [doFilter,28] springboot-server 7209831467195902001 958235974016818257 - START /thread
host localhost:8086
connection Keep-Alive
user-agent Apache-HttpClient/4.5.14 (Java/17.0.7)
accept-encoding br,deflate,gzip,x-gzip
2023-10-23 11:33:10.009 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,277] springboot-server 7209831467195902001 2587871298938674772 - this func is threadTest.
2023-10-23 11:33:10.022 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.CalcFilter - [doFilter,34] springboot-server 7209831467195902001 958235974016818257 - END: /thread Time consuming: 39

There is trace information generated in the log. 7209831467195902001 is traceId, and 2587871298938674772 is spanId.

Add new Thread to this interface to create a thread.

 @RequestMapping("/thread")
    @ResponseBody
    public String threadTest(){<!-- -->
        logger.info("this func is threadTest.");
        new Thread(()->{<!-- -->
            logger.info("this is new Thread.");
        }).start();
        return "success";
    }

Observe the log output by requesting the corresponding URL.

2023-10-23 11:40:00.994 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,277] springboot-server 319673369251953601 5380270359912403278 - this fun c is threadTest.
2023-10-23 11:40:00.995 [Thread-10] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,279] springboot-server - this is new Thread.

Through the log output, we found that the new Thread method cannot output Trace information, which means that Trace is not passed in.

Is it okay if we explicitly pass the Trace information in? Just do it.

Why doesn’t ThreadLocal work?

ThreadLocal Local thread variable, this variable is unique to the current thread.

For ease of use, we create a tool class ThreadLocalUtil.

public static final ThreadLocal<Span> THREAD_LOCAL = new ThreadLocal<>();

The current Span information is then stored in ThreadLocal.

 @RequestMapping("/thread")
    @ResponseBody
    public String threadTest(){<!-- -->
        logger.info("this func is threadTest.");
        ThreadLocalUtil.setValue(GlobalTracer.get().activeSpan());
        logger.info("current traceiD:{}",GlobalTracer.get().activeSpan().context().toTraceId());

        new Thread(()->{<!-- -->
            logger.info("this is new Thread.");
            logger.info("new Thread get span:{}",ThreadLocalUtil.getValue());
        }).start();
        return "success";
    }

Observe the log output by requesting the corresponding URL.

2023-10-23 14:14:02.339 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,278] springboot-server 4492960774800816442 4097884453719637622 - this func is threadTest.
2023-10-23 14:14:02.340 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,280] springboot-server 4492960774800816442 4097884453719637622 - current tracei D:4492960774800816442
2023-10-23 14:14:02.341 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,283] springboot-server - this is new Thread.
2023-10-23 14:14:02.342 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,284] springboot-server - new Thread get span:null

Get the external thread ThreadLocal in the new thread, and the value obtained is null.

By analyzing the ThreadLocal source code, we found that when we use the set() method of ThreadLocal, ThreadLocal internally uses Thread.currentThread() serves as the key for data storage of ThreadLocal, that is to say, when obtaining variable information from a new thread, key has changed, so the value cannot be obtained.

public class ThreadLocal<T> {<!-- -->
    ...
   public void set(T value) {<!-- -->
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {<!-- -->
            map.set(this, value);
        } else {<!-- -->
            createMap(t, value);
        }
    }
    public T get() {<!-- -->
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {<!-- -->
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {<!-- -->
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    ...
}

InheritableThreadLocal

InheritableThreadLocal extends ThreadLocal to provide inheritance of values from parent threads to child threads: when a child thread is created, the child thread receives all inheritable thread locals that the parent thread has values for The initial value of the variable.

Official explanation:

This class extends ThreadLocal to provide inheritance of values from parent thread to child thread: when a child thread is created, the child receives initial values for all inheritable thread-local variables for which the parent has values. Normally the child's values will be identical to the parent's; however, the child's value can be made an arbitrary function of the parent's by overriding the childValue method in this class.
Inheritable thread-local variables are used in preference to ordinary thread-local variables when the per-thread-attribute being maintained in the variable (e.g., User ID, Transaction ID) must be automatically transmitted to any child threads that are created.
Note: During the creation of a new thread, it is possible to opt out of receiving initial values for inheritable thread-local variables.

For ease of use, we create a tool class InheritableThreadLocalUtil.java to store Span information

public static final InheritableThreadLocal<Span> THREAD_LOCAL = new InheritableThreadLocal<>();

Replace ThreadLocalUtil with InheritableThreadLocalUtil

@RequestMapping("/thread")
    @ResponseBody
    public String threadTest(){<!-- -->
        logger.info("this func is threadTest.");
        InheritableThreadLocalUtil.setValue(GlobalTracer.get().activeSpan());
        logger.info("current traceiD:{}",GlobalTracer.get().activeSpan().context().toTraceId());

        new Thread(()->{<!-- -->
            logger.info("this is new Thread.");
            logger.info("new Thread get span:{}",InheritableThreadLocalUtil.getValue());
        }).start();
        return "success";
    }

Observe the log output by requesting the corresponding URL.

2023-10-23 14:37:05.415 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,278] springboot-server 8754268856419787293 5276611939997441402 - this func is threadTest.
2023-10-23 14:37:05.416 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,280] springboot-server 8754268856419787293 5276611939997441402 - current tracei D:8754268856419787293
2023-10-23 14:37:05.416 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,283] springboot-server - this is new Thread.
2023-10-23 14:37:05.417 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,284] springboot-server - new Thread get span:datadog.trace.instrumentation.opentracing32 .OTSpan@712ad7e2

By observing the above log information, the span object address has been obtained inside the thread, but there is no Trace information output in the pattern part of the log. The reason is that DDTrace performs instrumentation processing on logback‘s getMDCPropertyMap() and getMdc() methods, and transfers the Trace information put into MDC.

 @Advice.OnMethodExit(suppress = Throwable.class)
    public static void onExit(
        @Advice.This ILoggingEvent event,
        @Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false)
            Map<String, String> mdc) {<!-- -->

      if (mdc instanceof UnionMap) {<!-- -->
        return;
      }

      AgentSpan.Context context =
          InstrumentationContext.get(ILoggingEvent.class, AgentSpan.Context.class).get(event);

      // Nothing to add so return early
      if (context == null & amp; & amp; !AgentTracer.traceConfig().isLogsInjectionEnabled()) {<!-- -->
        return;
      }

      Map<String, String> correlationValues = new HashMap<>(8);

      if (context != null) {<!-- -->
        DDTraceId traceId = context.getTraceId();
        String traceIdValue =
            InstrumenterConfig.get().isLogs128bTraceIdEnabled() & amp; & amp; traceId.toHighOrderLong() != 0
                ? traceId.toHexString()
                : traceId.toString();
        correlationValues.put(CorrelationIdentifier.getTraceIdKey(), traceIdValue);
        correlationValues.put(
            CorrelationIdentifier.getSpanIdKey(), DDSpanId.toString(context.getSpanId()));
      }else{<!-- -->
        AgentSpan span = activeSpan();
        if (span!=null){<!-- -->
          correlationValues.put(
              CorrelationIdentifier.getTraceIdKey(), span.getTraceId().toString());
          correlationValues.put(
              CorrelationIdentifier.getSpanIdKey(), DDSpanId.toString(span.getSpanId()));
        }
      }

      String serviceName = Config.get().getServiceName();
      if (null != serviceName & amp; & amp; !serviceName.isEmpty()) {<!-- -->
        correlationValues.put(Tags.DD_SERVICE, serviceName);
      }
      String env = Config.get().getEnv();
      if (null != env & amp; & amp; !env.isEmpty()) {<!-- -->
        correlationValues.put(Tags.DD_ENV, env);
      }
      String version = Config.get().getVersion();
      if (null != version & amp; & amp; !version.isEmpty()) {<!-- -->
        correlationValues.put(Tags.DD_VERSION, version);
      }

      mdc = null != mdc ? new UnionMap<>(mdc, correlationValues) : correlationValues;
    }

In order for the log of the newly created thread to also obtain the trace information of the parent thread, this can be achieved by creating a span. The span needs to be a child span< of the parent thread. /code> to complete the concatenation.

 new Thread(()->{<!-- -->
        logger.info("this is new Thread.");
        logger.info("new Thread get span:{}",InheritableThreadLocalUtil.getValue());
        Span span = null;
        try {<!-- -->
            Tracer tracer = GlobalTracer.get();
            span = tracer.buildSpan("thread")
                    .asChildOf(InheritableThreadLocalUtil.getValue())
                    .start();
            span.setTag("threadName", Thread.currentThread().getName());
            GlobalTracer.get().activateSpan(span);
            logger.info("thread:{}",span.context().toTraceId());
        }finally {<!-- -->
            if (span!=null) {<!-- -->
                span.finish();
            }
        }
    }).start();

Observe the log output by requesting the corresponding URL.

2023-10-23 14:51:28.969 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,278] springboot-server 2303424716416355903 7690232490489894572 - this func is threadTest.
2023-10-23 14:51:28.969 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,280] springboot-server 2303424716416355903 7690232490489894572 - current tracei D:2303424716416355903
2023-10-23 14:51:28.970 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,283] springboot-server - this is new Thread.
2023-10-23 14:51:28.971 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,284] springboot-server - new Thread get span:datadog.trace.instrumentation.opentracing32 .OTSpan@c3a1aae
2023-10-23 14:51:28.971 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,292] springboot-server - thread:2303424716416355903
2023-10-23 14:51:28.971 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,294] springboot-server 2303424716416355903 5766505477412800739 - thread:2303 424716416355903

Why does the pattern with two logs in the thread not output Trace information? The reason is that the span inside the current thread is created after the log is output. You only need to put the log below the span creation.

 new Thread(()->{<!-- -->
        Span span = null;
        try {<!-- -->
            Tracer tracer = GlobalTracer.get();
            span = tracer.buildSpan("thread")
                    .asChildOf(InheritableThreadLocalUtil.getValue())
                    .start();
            span.setTag("threadName", Thread.currentThread().getName());
            GlobalTracer.get().activateSpan(span);
            logger.info("this is new Thread.");
            logger.info("new Thread get span:{}",InheritableThreadLocalUtil.getValue());
            logger.info("thread:{}",span.context().toTraceId());
        }finally {<!-- -->
            if (span!=null) {<!-- -->
                span.finish();
            }
        }
    }).start();

Observe the log output by requesting the corresponding URL.

2023-10-23 15:01:00.490 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,278] springboot-server 472828375731745486 6076606716618097397 - this fun c is threadTest.
2023-10-23 15:01:00.491 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [threadTest,280] springboot-server 472828375731745486 6076606716618097397 - current traceId :472828375731745486
2023-10-23 15:01:00.492 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,291] springboot-server 472828375731745486 9214366589561638347 - this is new Thread.
2023-10-23 15:01:00.492 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,292] springboot-server 472828375731745486 9214366589561638347 - new Thread get span:data dog.trace.instrumentation .opentracing32.OTSpan@12fd40f0
2023-10-23 15:01:00.493 [Thread-9] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$threadTest$1,293] springboot-server 472828375731745486 9214366589561638347 - thread:47282 8375731745486

ExecutorService

Create an API and create ExecutorService objects through Executors.

 @RequestMapping("/execThread")
    @ResponseBody
    public String ExecutorServiceTest(){<!-- -->
        ExecutorService executor = Executors.newCachedThreadPool();
        logger.info("this func is ExecutorServiceTest.");
        executor.submit(()->{<!-- -->
            logger.info("this is executor Thread.");
        });
        return "ExecutorService";
    }

Observe the log output by requesting the corresponding URL.

2023-10-23 15:24:41.828 [http-nio-8086-exec-1] INFO com.zy.observable.ddtrace.controller.IndexController - [ExecutorServiceTest,309] springboot-server 2170215511602500482 437036622195882390 8-this func is ExecutorServiceTest.
2023-10-23 15:24:41.832 [pool-2-thread-1] INFO com.zy.observable.ddtrace.controller.IndexController - [lambda$ExecutorServiceTest$2,311] springboot-server 2170215511602500482 4370366221958823908 - this is executor Thread.

ExecutorService The thread pool method will automatically transmit Trace information. This automatic capability comes from DDTrace's implementation of burying operations on corresponding components.

JAVA supports link transfer for many thread component frameworks, such as: ForkJoinTask, ForkJoinPool, TimerTask, FutureTask code>, ThreadPoolExecutor, etc.

Source code

springboot-ddtrace-server

syntaxbug.com © 2021 All Rights Reserved.