Skip to content

[instrumentation-graphql] Custom resolvers (@Query, @ResolveField) not generating spans with Apollo Server 5 + NestJS #3362

@lidigobeyond

Description

@lidigobeyond

What version of OpenTelemetry are you using?

  • @opentelemetry/instrumentation-graphql: 0.56.0
  • @opentelemetry/sdk-node: 0.208.0
  • @opentelemetry/api: 1.9.0
  • @opentelemetry/instrumentation-http: 0.208.0
  • @opentelemetry/instrumentation-express: 0.57.0

What version of Node are you using?

Node.js 20.20.0

What did you do?

I set up OpenTelemetry with @opentelemetry/instrumentation-graphql in a NestJS application using
Apollo Server 5.

Environment:

  • @apollo/server: 5.1.0
  • @nestjs/graphql: 13.2.0
  • @nestjs/apollo: 13.2.1
  • graphql: 16.12.0

Instrumentation setup (instrumentation.ts):

import { NodeSDK } from '@opentelemetry/sdk-node';                                                
import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';                  
// ... other imports                                                                              
                                                                                                  
const sdk = new NodeSDK({                                                                         
  serviceName: 'hr-graphql-api',                                                                  
  traceExporter: new OTLPTraceExporter({                                                          
    url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,                                  
  }),                                                                                             
  instrumentations: [                                                                             
    new HttpInstrumentation(),                                                                    
    new ExpressInstrumentation(),                                                                 
    new GraphQLInstrumentation({}),                                                                                           
  ],                                                                                              
});                                                                                               
                                                                                                  
sdk.start();                                                                                      

Resolver definition:

@Resolver(() => Department)                                                                       
export class DepartmentResolver {                                                                 
  @Query(() => Department, { name: 'department', nullable: true })                                
  getById(@Args('id') id: string): Promise<Department | null> {                                   
    return this.departmentService.getById(id);                                                    
  }                                                                                               
                                                                                                    
  @ResolveField(() => Employees, { name: 'employees' })                                           
  listEmployees(                                                                                  
    @Parent() department: Department,                                                             
    @Args({ name: 'offset', type: () => Int, nullable: true }) offset = 0,                        
    @Args({ name: 'limit', type: () => Int, nullable: true }) limit = 10,                         
  ): Promise<Employees> {                                                                         
    return this.employeeService.listByDepartmentId(department.id, offset, limit);                 
  }                                                                                               
}                                                                                                 

GraphQL query executed:

query getDepartment {                                                                                
  department(id: "d009") {                                                                        
    id                                                                                            
    name                                                                                           
    employees {                                                                                   
      items {                                                                                     
        id                                                                                        
        lastName                                                                                  
        firstName                                                                                 
      }                                                                                           
    }                                                                                             
  }                                                                                               
}                                                                                                 

Reproduction repository:

https://github.com/lidigobeyond/graphql-otel-in-action

Steps to reproduce:

  1. Clone the repository
git clone --recurse-submodules https://github.com/lidigobeyond/graphql-otel-in-action.git                              
cd graphql-otel-in-action                                                                         
  1. Start the application with Docker Compose
docker-compose up --build                                      

Note: Initial database setup may take 1-2 minutes.

  1. Open GraphQL Playground at http://localhost:3000/api/graphql
  2. Execute the following query:
query getDepartment {                                                                                
  department(id: "d009") {                                                                        
    id                                                                                            
    name                                                                                          
    employees {                                                                                   
      items {                                                                                     
        id                                                                                        
        lastName                                                                                  
        firstName                                                                                 
      }                                                                                           
    }                                                                                             
  }                                                                                               
}                                                                                                 
  1. Open Jaeger UI at http://localhost:16686 and inspect the trace

What did you expect to see?

I expected to see spans for:

  • graphql.resolve department - the root Query resolver (getById method)
  • graphql.resolve department.employees - the ResolveField resolver (listEmployees method)

Expected span hierarchy:
POST /api/graphql
└── query getDepartment
├── graphql.resolve department <-- Expected but missing
├── graphql.resolve department.id
├── graphql.resolve department.name
└── graphql.resolve department.employees <-- Expected but missing
└── graphql.resolve department.employees.items

What did you see instead?

Custom resolvers decorated with @query and @ResolveField do not generate spans.
Only trivial scalar field resolvers (id, name, firstName, lastName) generate spans.

Actual span hierarchy:
POST /api/graphql
└── query department
├── graphql.resolve department.id <-- Only scalar fields have spans
├── graphql.resolve department.name
└── graphql.resolve department.employees.items
├── graphql.resolve department.employees.items..id
├── graphql.resolve department.employees.items.
.lastName
└── graphql.resolve department.employees.items.*.firstName

The graphql.resolve department and graphql.resolve department.employees spans are completely
missing.

Image

Tip:

https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/
with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1
or me too, to help us triage it.
https://opentelemetry.io/community/end-user/issue-participation/.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpkg:instrumentation-graphqlpriority:p2Bugs and spec inconsistencies which cause telemetry to be incomplete or incorrect

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions