Dynatrace’s Code-Level Visibility can pinpoint method-level performance bottlenecks, but the trick is knowing how to ask for the right data.

Let’s see it in action. Imagine a Java web application running on Tomcat. You’ve got a suspicious API endpoint that’s occasionally slow. You’d typically dive into Dynatrace’s PurePaths to see the trace, but you want to go deeper, to the specific Java methods causing the pain.

Here’s a sample transaction. A user requests /api/v1/products/123. Dynatrace captures this.

GET /api/v1/products/123
  -> com.example.ProductService.getProductById(123) (150ms)
    -> com.example.ProductRepository.findById(123) (100ms)
      -> java.sql.Connection.prepareStatement("SELECT * FROM products WHERE id = ?") (5ms)
      -> java.sql.PreparedStatement.execute() (80ms)
        -> JDBC Driver: Fetching data from database... (75ms)
    -> com.example.ProductMapper.toProductDTO(productData) (20ms)
  -> com.example.ProductController.getProductById(product) (170ms)

This shows a basic trace. getProductById in ProductService took 150ms. But why? Was it the database call, or something else within that method? Dynatrace’s Code-Level Visibility, enabled via its OneAgent, can break this down further.

The core concept here is method invocation analysis. Dynatrace’s OneAgent, when configured for code-level visibility, instruments your application’s bytecode. It doesn’t just time entire methods; it records the time spent within each method and the time spent waiting for other methods it calls. This gives you a precise breakdown of execution time.

To get this level of detail, you need to ensure your OneAgent has code-level visibility enabled. This is typically done through OneAgent configuration. For Java, this often involves modifying the dt_java_agent.ini file or using specific startup arguments. A common setting to enable deep code analysis might look like this in your catalina.sh or equivalent startup script:

-agentpath:/opt/dynatrace/oneagent/lib/codelib/codelib.jar=--set=codelib.enabled=true --set=codelib.java.enabled=true

The codelib.enabled=true and codelib.java.enabled=true flags are the magic. They tell the agent to perform bytecode instrumentation for detailed method-level insights. Once enabled and your application restarts, Dynatrace’s UI will start showing more granular data.

In the Dynatrace UI, navigate to the problematic PurePath. You’ll see the existing method breakdown. Now, if you expand com.example.ProductService.getProductById, you’ll see even finer-grained details. Instead of just the 150ms for the whole method, you might see:

com.example.ProductService.getProductById(123) (150ms)
  - Self-time: 30ms (code executed directly within getProductById)
  - Called com.example.ProductRepository.findById(123): 100ms
    - Self-time: 10ms (code executed directly within findById)
    - Called java.sql.Connection.prepareStatement(...): 5ms
    - Called java.sql.PreparedStatement.execute(): 80ms
      - Self-time: 5ms
      - JDBC Driver: Fetching data from database...: 75ms
  - Called com.example.ProductMapper.toProductDTO(productData): 20ms

The "Self-time" is crucial. It represents the time spent executing the code within that specific method, excluding time spent waiting for or executing calls to other methods. This helps distinguish between slow application logic and slow external dependencies.

You can control the depth and scope of this analysis. For instance, if you’re only interested in specific packages or classes, you can configure OneAgent to only instrument those, reducing overhead. This is managed through OneAgent’s instrumentation.filters settings, often configured via the Dynatrace UI or configuration files. A filter might look like:

{
  "codelib": {
    "java": {
      "instrumentation": {
        "filters": [
          {
            "include": [
              "com.example.ProductService.*",
              "com.example.ProductRepository.*"
            ]
          }
        ]
      }
    }
  }
}

This tells Dynatrace to only instrument methods within com.example.ProductService and com.example.ProductRepository. This is a powerful way to focus performance analysis without instrumenting your entire application, minimizing performance impact.

The most surprising aspect of Dynatrace’s code-level visibility is how it distinguishes between "self-time" and "wait time" for asynchronous operations. It doesn’t just show you that a method took 200ms; it tells you if that 200ms was 190ms of your code doing work and 10ms waiting for a database, or if it was 10ms of your code and 190ms spent blocked on a network call or a thread pool. This is achieved by tracking thread states and context switches at a very granular level, correlating them back to specific method invocations.

The next step after identifying hot methods is often understanding why those methods are slow. This could lead you into analyzing garbage collection pauses, thread contention, or inefficient algorithm implementations within those methods.

Want structured learning?

Take the full Dynatrace course →