Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to enable/disable segment generation at runtime #70

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
.nyc_output/
coverage/

src/**/*.js
test/**/*.js
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,20 @@ AWS has a 64K upload limit when submitting segements to AWS see [AWSXRay concept
If you try and submit more than this limit you will see the following aws error message<br>**"<date> [ERROR] Segment too large to send: {<traceinformation...}"**

One approach to remove this error, is to reduce the size of the batch upload (eg.. setStreamingThreshold(0) which will send each subsegment on close). See [SDK - nodejs - setStreamingThreshold](https://docs.aws.amazon.com/xray-sdk-for-nodejs/latest/reference/AWSXRay.html).

## Runtime Options

To add the ability to enable/disable segment creation at runtime, pass in an options object like so:

```javascript
const traceResolvers = require('@lifeomic/graphql-resolvers-xray-tracing');
const schema = makeExecutableSchema( ... );

const traceOptions = {
enabled: true
};

traceResolvers(schema, traceOptions);
```

traceOptions, if declared at the module level, can then be used to programatically turn on/off segment creation.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"posttest": "yarn lint && tsc --noEmit",
"postinstall": "if [ -d test ]; then yarn types; fi",
"coverage": "nyc report --reporter=text-lcov > ./.nyc_output/lcov.info",
"coverage_report": "nyc --reporter=lcov --reporter=text-summary ava",
"prepublishOnly": "yarn tsc",
"cleanTypes": "rm -rf test/__generated__"
},
Expand Down
38 changes: 25 additions & 13 deletions src/traceResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,32 @@ function fieldPathFromInfo (info: GraphQLResolveInfo) {
return segments.join('.');
}

export default <TSource = any, TContext = any, TArgs = any>(schema: GraphQLSchema): GraphQLSchemaWithFragmentReplacements => {
export interface TraceOptions {
enabled: boolean;
}

const DefaultOptions: TraceOptions = {
enabled: true
};

export default <TSource = any, TContext = any, TArgs = any>(schema: GraphQLSchema, options: TraceOptions = DefaultOptions): GraphQLSchemaWithFragmentReplacements => {
const tracer: IMiddlewareResolver<TSource, TContext, TArgs> = async (resolver, _parent, _args, _ctx, info) => {
const fieldPath = fieldPathFromInfo(info);
return AWSXRay.captureAsyncFunc(`GraphQL ${fieldPath}`, async (subsegment) => {
// When AWS_XRAY_CONTEXT_MISSING is set to LOG_MISSING and no context is found then subsegment is undefined.
try {
const result = await resolver();
subsegment?.close();
return result;
} catch (error: any) {
subsegment?.close(error);
throw error;
}
});
if (options.enabled) {
const fieldPath = fieldPathFromInfo(info);
return AWSXRay.captureAsyncFunc(`GraphQL ${fieldPath}`, async (subsegment) => {
// When AWS_XRAY_CONTEXT_MISSING is set to LOG_MISSING and no context is found then subsegment is undefined.
try {
const result = await resolver();
subsegment?.close();
return result;
} catch (error: any) {
subsegment?.close(error);
throw error;
}
});
} else {
return resolver();
}
};

return applyMiddlewareToDeclaredResolvers(schema, tracer);
Expand Down
8 changes: 6 additions & 2 deletions test/helpers/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { makeExecutableSchema } from 'graphql-tools';
import { v4 as uuid } from 'uuid';
import { loadFiles } from '@graphql-toolkit/file-loading';
import path from 'path';
import traceResolvers from '../../src/traceResolvers';
import traceResolvers, { TraceOptions } from '../../src/traceResolvers';

const blocked = new Map();

Expand Down Expand Up @@ -80,10 +80,14 @@ const resolvers = {
}
};

export function traceSchema () {
export function traceSchema (options: TraceOptions|null) {
const schema = makeExecutableSchema({
typeDefs: loadFiles(path.join(__dirname, '**/*.graphql')),
resolvers
});

if (options) {
return traceResolvers(schema, options);
}
return traceResolvers(schema);
}
6 changes: 5 additions & 1 deletion test/traceResolvers-contextMissing.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { graphql, GraphQLError, Source } from 'graphql';
import { traceSchema } from './helpers/schema';
import { TraceOptions } from '../src/traceResolvers';
import nock from 'nock';
import anyTest, { TestInterface } from 'ava';
import AWSXRay from 'aws-xray-sdk-core';
Expand All @@ -25,11 +26,14 @@ interface TestContext {
}

const test = anyTest as TestInterface<TestContext>;
const traceOptions: TraceOptions = {
enabled: true
};

test.beforeEach((t) => {
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');
const schema = traceSchema();
const schema = traceSchema(traceOptions);

t.context.graphql = (query) => graphql(schema, query);
});
Expand Down
114 changes: 114 additions & 0 deletions test/traceResolvers-withOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { graphql } from 'graphql';
import { traceSchema } from './helpers/schema';
import { TraceOptions } from '../src/traceResolvers';
import nock from 'nock';
import anyTest, { TestInterface } from 'ava';
import AWSXRay, { Segment } from 'aws-xray-sdk-core';
import { ExecutionResult, ExecutionResultDataDefault } from 'graphql/execution/execute';

AWSXRay.capturePromise();

type GraphQlQuery = Parameters<typeof graphql>[1];
type Namespace = ReturnType<typeof AWSXRay.getNamespace>;

interface TestContext {
ns: Namespace;
segment: Segment;
graphql: <TData = ExecutionResultDataDefault>(query: GraphQlQuery) => Promise<ExecutionResult<TData>>;
}

const test = anyTest as TestInterface<TestContext>;
const traceOptions: TraceOptions = {
enabled: true
};

test.beforeEach(function (test) {
const schema = traceSchema(traceOptions);
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');

const ns: Namespace = AWSXRay.getNamespace();
test.context.ns = ns;

const segment = new AWSXRay.Segment('parent');
test.context.segment = segment;

test.context.graphql = ns.bind(function (query: GraphQlQuery) {
AWSXRay.setSegment(segment);
try {
return graphql(schema, query);
} finally {
segment.close();
}
});
});

test.afterEach.always(() => {
nock.enableNetConnect();
nock.cleanAll();
});

test('Trace segments are not created for resolvers when enabled is not true', async function (test) {
try {
traceOptions.enabled = false;
const { segment, graphql } = test.context;
const result = await graphql('{ hello }');

if (result.errors) {
throw result.errors[0];
}
test.deepEqual(result, {
data: {
hello: 'world'
}
});

test.is(segment.subsegments, undefined);
} finally {
traceOptions.enabled = true;
}
});

const pre = (test: any, options: TraceOptions|null) => {
const schema = traceSchema(options);
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');

const ns: Namespace = AWSXRay.getNamespace();
test.context.ns = ns;

const segment = new AWSXRay.Segment('parent');
test.context.segment = segment;

test.context.graphql = ns.bind(function (query: GraphQlQuery) {
AWSXRay.setSegment(segment);
try {
return graphql(schema, query);
} finally {
segment.close();
}
});
};

test('Trace segments are created for resolvers when defaults are used', async function (test) {
try {
pre(test, null);

const { segment, graphql } = test.context;
const result = await graphql('{ hello }');

if (result.errors) {
throw result.errors[0];
}
test.deepEqual(result, {
data: {
hello: 'world'
}
});

test.is(segment.subsegments?.length, 1);
} finally {
nock.enableNetConnect();
nock.cleanAll();
}
});
6 changes: 5 additions & 1 deletion test/traceResolvers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { graphql } from 'graphql';
import { traceSchema } from './helpers/schema';
import { TraceOptions } from '../src/traceResolvers';
import nock from 'nock';
import anyTest, { ExecutionContext, TestInterface } from 'ava';
import AWSXRay, { Segment, Subsegment } from 'aws-xray-sdk-core';
Expand All @@ -19,9 +20,12 @@ interface TestContext {
}

const test = anyTest as TestInterface<TestContext>;
const traceOptions: TraceOptions = {
enabled: true
};

test.beforeEach(function (test) {
const schema = traceSchema();
const schema = traceSchema(traceOptions);
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');

Expand Down