diff --git a/packages/plugins/appdynamics/package.json b/packages/plugins/appdynamics/package.json new file mode 100644 index 000000000..5cf698106 --- /dev/null +++ b/packages/plugins/appdynamics/package.json @@ -0,0 +1,61 @@ +{ + "name": "@graphql-mesh/plugin-appdynamics", + "version": "0.0.0", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/graphql-hive/gateway.git", + "directory": "packages/plugins/appdynamics" + }, + "author": { + "email": "contact@the-guild.dev", + "name": "The Guild", + "url": "https://the-guild.dev" + }, + "license": "MIT", + "engines": { + "node": ">=18.0.0 <23.0.0" + }, + "main": "./dist/index.js", + "exports": { + ".": { + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "pkgroll --clean-dist", + "prepack": "yarn build" + }, + "peerDependencies": { + "graphql": "^15.9.0 || ^16.9.0" + }, + "dependencies": { + "@graphql-hive/gateway-runtime": "workspace:^", + "@graphql-mesh/cross-helpers": "^0.4.8", + "@graphql-mesh/store": "^0.103.4", + "@graphql-mesh/transport-common": "workspace:^", + "@graphql-mesh/types": "^0.103.4", + "@graphql-mesh/utils": "^0.103.4", + "@graphql-tools/utils": "^10.6.0", + "@whatwg-node/disposablestack": "^0.0.5", + "tslib": "^2.4.0" + }, + "devDependencies": { + "graphql": "^16.9.0", + "graphql-yoga": "^5.7.0", + "pkgroll": "2.5.1" + }, + "sideEffects": false +} diff --git a/packages/plugins/appdynamics/src/appdynamics.d.ts b/packages/plugins/appdynamics/src/appdynamics.d.ts new file mode 100644 index 000000000..b9752fd1f --- /dev/null +++ b/packages/plugins/appdynamics/src/appdynamics.d.ts @@ -0,0 +1,71 @@ +import type { ClientRequest } from 'node:http'; + +export interface Agent { + startTransaction( + correlationInfo?: string | HTTPRequest | CorrelationHeader | null, + cb?: (tx: TimePromise) => void, + ): TimePromise; + + getTransaction(req: ClientRequest): TimePromise; + + parseCorrelationInfo(source: string | HTTPRequest): CorrelationHeader; + + __agent: { + correlation: { + HEADER_NAME: string; + }; + }; +} + +/** + * Transaction handle + */ +export interface TimePromise { + resume(): void; + start(cb: (tp: TimpePromise) => void); + markError(err: Error, statusCode?: number): void; + end(err?: Error, statusCode?: number): void; + startExitCall(exitCallInfo: ExitCallInfo): ExitCall; + endExitCall(exitCall: ExitCall): void; + createCorrelationInfo(exitCall, doNotResolve?: boolean): CorrelationHeader; + addSnapshotData(key: string, value: unknown): void; + addAnalyticsData(key: string, value: unknown): void; + + // callbacks + beforeExitCall(exitCall: ExitCall): ExitCall; +} + +export type HTTPRequest = { + headers?: { + singularityheader?: string; + }; +}; + +export type CorrelationHeader = { + businessTransactionName: string; + headers: { + singularityheader: string; + }; +}; + +export type ExitCall = { + exitType: 'EXIT_HTTP'; + /** + * @default "HTTP" + */ + backendName: string; + label: string; + method: string; + requestHeaders: Record; + responseHeaders: Record; + statusCode: number; + category: 'read' | 'write'; + /** + * URL of the request + */ + command: string; + identifyingProperties: { + HOST: string; + PORT: string; + }; +}; diff --git a/packages/plugins/appdynamics/src/index.ts b/packages/plugins/appdynamics/src/index.ts new file mode 100644 index 000000000..e026cd5bb --- /dev/null +++ b/packages/plugins/appdynamics/src/index.ts @@ -0,0 +1,51 @@ +import { type GatewayPlugin } from '@graphql-hive/gateway-runtime'; +import type { Logger } from '@graphql-mesh/types'; +import { Agent, TimePromise } from './appdynamics'; + +type AppDynamicsPluginOptions = { + logger: Logger; + appd: Agent; +}; + +export default function useAppDynamics( + options: AppDynamicsPluginOptions, +): GatewayPlugin { + const logger = options.logger.child('AppDynamics'); + const txByRequest = new WeakMap(); + const appd = options.appd; + + return { + //@ts-expect-error TODO: how to declare this actually exists if we are running on Node ? + onRequest({ request, serverContext: { req } }) { + try { + const tx = + appd.getTransaction(req) ?? + appd.startTransaction( + request.headers.get(appd.__agent.correlation.HEADER_NAME), + ); + txByRequest.set(request, tx); + } catch (err) { + logger.error('failed to get or start transaction:', err); + } + }, + onFetch({ context: { request }, fetchFn, setFetchFn }) { + const tx = txByRequest.get(request); + if (!tx) { + return; + } + + setFetchFn((...args) => { + tx.resume(); // Not sure it is needed, let's see if it's working with it, and try to remove it to see the effect + return fetchFn(...args); + }); + }, + onResponse({ request }) { + try { + const tx = txByRequest.get(request); + tx?.end(); + } catch (err) { + logger.error('failed to end the transaction', err); + } + }, + }; +} diff --git a/yarn.lock b/yarn.lock index a15fe83e8..8872b80fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3485,6 +3485,27 @@ __metadata: languageName: node linkType: hard +"@graphql-mesh/plugin-appdynamics@workspace:packages/plugins/appdynamics": + version: 0.0.0-use.local + resolution: "@graphql-mesh/plugin-appdynamics@workspace:packages/plugins/appdynamics" + dependencies: + "@graphql-hive/gateway-runtime": "workspace:^" + "@graphql-mesh/cross-helpers": "npm:^0.4.8" + "@graphql-mesh/store": "npm:^0.103.4" + "@graphql-mesh/transport-common": "workspace:^" + "@graphql-mesh/types": "npm:^0.103.4" + "@graphql-mesh/utils": "npm:^0.103.4" + "@graphql-tools/utils": "npm:^10.6.0" + "@whatwg-node/disposablestack": "npm:^0.0.5" + graphql: "npm:^16.9.0" + graphql-yoga: "npm:^5.7.0" + pkgroll: "npm:2.5.1" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^15.9.0 || ^16.9.0 + languageName: unknown + linkType: soft + "@graphql-mesh/plugin-deduplicate-request@npm:^0.103.0": version: 0.103.5 resolution: "@graphql-mesh/plugin-deduplicate-request@npm:0.103.5"