Skip to content

Commit

Permalink
Fix Apollo server error reporting
Browse files Browse the repository at this point in the history
Ensure `setRoot` sets an active scope. If there is no active
scope, calling `scopeManager.setRoot` will set the given root
span as both the active span and the root span for the current
process. This fixes a bug where processes lose track of their
root span after `tracer.withSpan` is called.

Remove a check in the HTTP outgoing instrumentation that checks
if there is a current span before creating a root span.

Add integration tests for the current behaviour of the Apollo
Server integration. It can report a successful query, including
the query body, and report errors that take place during the
parsing and validation stages.

The error reporting was previously not working due to the bug
described above regarding `scopeManager.setRoot`.
  • Loading branch information
unflxw committed Apr 7, 2022
1 parent 3fb6b3a commit 817fdb4
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 32 deletions.
6 changes: 6 additions & 0 deletions packages/apollo-server/.changesets/fix-error-reporting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
bump: "patch"
type: "fix"
---

Fix error reporting. Due to a bug in the scope management logic, errors emitted by Apollo Server were not being reported by the integration.
8 changes: 6 additions & 2 deletions packages/apollo-server/test/example/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { Appsignal } = require("../../../nodejs")

new Appsignal({
const appsignal = new Appsignal({
active: true,
name: "<YOUR APPLICATION NAME>",
pushApiKey: "<YOUR API KEY>"
Expand Down Expand Up @@ -47,9 +47,13 @@ const resolvers = {
}
}

const { createApolloPlugin } = require("../../../apollo-server")

const plugins = [createApolloPlugin(appsignal)]

// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
const server = new ApolloServer({ typeDefs, resolvers })
const server = new ApolloServer({ typeDefs, resolvers, plugins })

// The `listen` method launches a web server.
server.listen({ port: 4010 }).then(({ url }) => {
Expand Down
127 changes: 109 additions & 18 deletions packages/apollo-server/test/spec/integration_spec.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# frozen_string_literal: true

require "net/http"
require "json"

EXAMPLE_APP_DIR = File.expand_path(File.join("..", "example"), __dir__)

RSpec.describe "Next.js" do
RSpec.describe "Apollo Server" do
before(:context) do
@app = AppRunner.new("npm run start", EXAMPLE_APP_DIR)
@app.run
Expand All @@ -13,22 +14,112 @@
after(:context) { @app.stop }
after { @app.cleanup }

describe "POST /" do
before do
uri = URI("http://localhost:4010/")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri)
request["Content-Type"] = "application/json"
request.body = '{"query":"query { __typename }"}'
@response = http.request(request).body
end

it "sets the root span's name" do
expect(@response).to match('{"data":{"__typename":"Query"}}')

log = @app.logs
span_id = fetch_root_span_id(log)
expect(log).to include("Set name 'POST /' for span '#{span_id}'")
end
def request_query(query)
uri = URI("http://localhost:4010/")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri)
request["Content-Type"] = "application/json"
request.body = { "query" => query }.to_json
http.request(request)
end

it("reports a successful query") do
query = "query AllTheBooks { books { title } }"
response = request_query(query)
expect(response.code).to eq("200")

log = @app.logs

root_span_id = fetch_root_span_id(log)
expect(log).to include("Set name 'AllTheBooks' for span '#{root_span_id}'")

child_span_ids = fetch_child_span_ids(log, root_span_id)
expect(child_span_ids.length).to be(3)

parse_span_id, validate_span_id, execute_span_id = child_span_ids

expect(log).to include(
"Set attribute for key 'appsignal:category' " \
"with value 'parse.graphql' to span '#{parse_span_id}'"
)
expect(log).to include(
"Set attribute for key 'appsignal:category' " \
"with value 'validate.graphql' to span '#{validate_span_id}'"
)
expect(log).to include(
"Set attribute for key 'appsignal:category' " \
"with value 'execute.graphql' to span '#{execute_span_id}'"
)
expect(log).to include(
"Set attribute for key 'appsignal:body' " \
"with value '#{query}' to span '#{execute_span_id}'"
)
end

it("reports an error when a query fails to parse") do
query = "gibberish"
response = request_query(query)
expect(response.code).to eq("400")

log = @app.logs

root_span_id = fetch_root_span_id(log)
expect(log).to include("Set name '[unknown graphql query]' for span '#{root_span_id}'")

expect(log).to include(
"Add error 'GraphQLError' to span '#{root_span_id}' with message " \
"'Syntax Error: Unexpected Name \"gibberish\".' with a backtrace"
)

child_span_ids = fetch_child_span_ids(log, root_span_id)
expect(child_span_ids.length).to be(1)

parse_span_id = child_span_ids.first

expect(log).to include(
"Set attribute for key 'appsignal:category' " \
"with value 'parse.graphql' to span '#{parse_span_id}'"
)

expect(log).not_to include(
"Set attribute for key 'appsignal:category' with value 'validate.graphql'"
)
expect(log).not_to include(
"Set attribute for key 'appsignal:category' with value 'execute.graphql'"
)
end

it("reports an error when a query fails to validate") do
query = "query { books { cover } }"
response = request_query(query)
expect(response.code).to eq("400")

log = @app.logs

root_span_id = fetch_root_span_id(log)
expect(log).to include("Set name '[unknown graphql query]' for span '#{root_span_id}'")

expect(log).to include(
"Add error 'GraphQLError' to span '#{root_span_id}' with message " \
"'Cannot query field \"cover\" on type \"Book\".' with a backtrace"
)

child_span_ids = fetch_child_span_ids(log, root_span_id)
expect(child_span_ids.length).to be(2)

parse_span_id, validate_span_id = child_span_ids

expect(log).to include(
"Set attribute for key 'appsignal:category' " \
"with value 'parse.graphql' to span '#{parse_span_id}'"
)
expect(log).to include(
"Set attribute for key 'appsignal:category' " \
"with value 'validate.graphql' to span '#{validate_span_id}'"
)

expect(log).not_to include(
"Set attribute for key 'appsignal:category' with value 'execute.graphql'"
)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
bump: "patch"
type: "fix"
---

Ensure the root span is preserved across scopes. Due to a bug in the scope management logic, calling `tracer.withSpan` could cause the root span for a given scope to be forgotten.
23 changes: 23 additions & 0 deletions packages/nodejs/src/__tests__/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ describe("ScopeManager", () => {
})
})

describe(".setRoot()", () => {
it("sets the root span and active span for the current process", () => {
const span = new RootSpan()

scopeManager.setRoot(span)

expect(scopeManager.root()).toStrictEqual(span)
expect(scopeManager.active()).toStrictEqual(span)
})

describe("when there is an active span", () => {
it("does not set the active span for the current process", () => {
const rootSpan = new RootSpan()
const childSpan = new ChildSpan(rootSpan)

scopeManager.withContext(childSpan, () => {
scopeManager.setRoot(rootSpan)
expect(scopeManager.active()).toStrictEqual(childSpan)
})
})
})
})

describe(".withContext()", () => {
it("should run the callback (object as target)", () => {
const fn = jest.fn(() => {
Expand Down
15 changes: 3 additions & 12 deletions packages/nodejs/src/instrumentation/http/lifecycle/outgoing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
* Copyright 2019, OpenTelemetry Authors
*/

import { Tracer, Span } from "../../../interfaces"
import { Tracer } from "../../../interfaces"
import url from "url"
import { IncomingMessage, ClientRequest, RequestOptions } from "http"

import { NoopSpan } from "../../../noops"

type HttpRequestArgs = Array<
(
res: IncomingMessage
Expand Down Expand Up @@ -72,7 +70,6 @@ function outgoingRequestFunction(
urlOrOptions: string | url.URL | RequestOptions,
...args: unknown[]
): ClientRequest {
let span: Span
let origin: string
let method: string

Expand All @@ -91,14 +88,8 @@ function outgoingRequestFunction(
return original.apply(this, [urlOrOptions, ...args])
}

if (tracer.currentSpan() instanceof NoopSpan) {
// create a new `RootSpan` if there isn't one already in progress
span = tracer.createSpan()
} else {
span = tracer.currentSpan().child()
}

span
const span = tracer
.createSpan()
.setName(`${method} ${origin}`)
.setCategory("request.http")
.set("method", method)
Expand Down
3 changes: 3 additions & 0 deletions packages/nodejs/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ export class ScopeManager {
public setRoot(rootSpan: Span) {
const uid = asyncHooks.executionAsyncId()
this.#roots.set(uid, rootSpan)
if (!this.#scopes.has(uid)) {
this.#scopes.set(uid, rootSpan)
}
}

/*
Expand Down
4 changes: 4 additions & 0 deletions test/integration/support/integration_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ def fetch_root_span_id(log)

span_id
end

def fetch_child_span_ids(log, root_span_id)
log.scan(/Start child span '(\w+)' with parent '#{root_span_id}'/).map(&:first)
end
end

0 comments on commit 817fdb4

Please sign in to comment.