feature: add tracing to remote query (#9128)
This commit is contained in:
@@ -20,6 +20,38 @@ import {
|
|||||||
export class Query {
|
export class Query {
|
||||||
#remoteQuery: RemoteQuery
|
#remoteQuery: RemoteQuery
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to wrap execution of the graph query for instrumentation
|
||||||
|
*/
|
||||||
|
static traceGraphQuery?: (
|
||||||
|
queryFn: () => Promise<any>,
|
||||||
|
queryOptions: RemoteQueryObjectConfig<any>
|
||||||
|
) => Promise<any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to wrap execution of the remoteQuery overload function
|
||||||
|
* for instrumentation
|
||||||
|
*/
|
||||||
|
static traceRemoteQuery?: (
|
||||||
|
queryFn: () => Promise<any>,
|
||||||
|
queryOptions:
|
||||||
|
| RemoteQueryObjectConfig<any>
|
||||||
|
| RemoteQueryObjectFromStringResult<any>
|
||||||
|
| RemoteJoinerQuery
|
||||||
|
) => Promise<any>
|
||||||
|
|
||||||
|
static instrument = {
|
||||||
|
graphQuery(tracer: (typeof Query)["traceGraphQuery"]) {
|
||||||
|
Query.traceGraphQuery = tracer
|
||||||
|
},
|
||||||
|
remoteQuery(tracer: (typeof Query)["traceRemoteQuery"]) {
|
||||||
|
Query.traceRemoteQuery = tracer
|
||||||
|
},
|
||||||
|
remoteDataFetch(tracer: (typeof RemoteQuery)["traceFetchRemoteData"]) {
|
||||||
|
RemoteQuery.traceFetchRemoteData = tracer
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
constructor(remoteQuery: RemoteQuery) {
|
constructor(remoteQuery: RemoteQuery) {
|
||||||
this.#remoteQuery = remoteQuery
|
this.#remoteQuery = remoteQuery
|
||||||
}
|
}
|
||||||
@@ -76,6 +108,13 @@ export class Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = this.#unwrapQueryConfig(queryOptions)
|
const config = this.#unwrapQueryConfig(queryOptions)
|
||||||
|
if (Query.traceRemoteQuery) {
|
||||||
|
return await Query.traceRemoteQuery(
|
||||||
|
async () => this.#remoteQuery.query(config, undefined, options),
|
||||||
|
queryOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return await this.#remoteQuery.query(config, undefined, options)
|
return await this.#remoteQuery.query(config, undefined, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +137,27 @@ export class Query {
|
|||||||
options?: RemoteJoinerOptions
|
options?: RemoteJoinerOptions
|
||||||
): Promise<GraphResultSet<TEntry>> {
|
): Promise<GraphResultSet<TEntry>> {
|
||||||
const normalizedQuery = remoteQueryObjectFromString(queryOptions).__value
|
const normalizedQuery = remoteQueryObjectFromString(queryOptions).__value
|
||||||
const response = await this.#remoteQuery.query(
|
let response:
|
||||||
normalizedQuery,
|
| any[]
|
||||||
undefined,
|
| { rows: any[]; metadata: RemoteQueryFunctionReturnPagination }
|
||||||
options
|
|
||||||
)
|
/**
|
||||||
|
* When traceGraphQuery method is defined, we will wrap the implementation
|
||||||
|
* inside a callback and provide the method to the traceGraphQuery
|
||||||
|
*/
|
||||||
|
if (Query.traceGraphQuery) {
|
||||||
|
response = await Query.traceGraphQuery(
|
||||||
|
async () =>
|
||||||
|
this.#remoteQuery.query(normalizedQuery, undefined, options),
|
||||||
|
queryOptions
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
response = await this.#remoteQuery.query(
|
||||||
|
normalizedQuery,
|
||||||
|
undefined,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return this.#unwrapRemoteQueryResponse(response)
|
return this.#unwrapRemoteQueryResponse(response)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ export class RemoteQuery {
|
|||||||
private modulesMap: Map<string, LoadedModule> = new Map()
|
private modulesMap: Map<string, LoadedModule> = new Map()
|
||||||
private customRemoteFetchData?: RemoteFetchDataCallback
|
private customRemoteFetchData?: RemoteFetchDataCallback
|
||||||
|
|
||||||
|
static traceFetchRemoteData?: (
|
||||||
|
fetcher: () => Promise<any>,
|
||||||
|
serviceName: string,
|
||||||
|
method: string,
|
||||||
|
options: { select?: string[]; relations: string[] }
|
||||||
|
) => Promise<any>
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
modulesLoaded,
|
modulesLoaded,
|
||||||
customRemoteFetchData,
|
customRemoteFetchData,
|
||||||
@@ -227,7 +234,17 @@ export class RemoteQuery {
|
|||||||
options.take = null
|
options.take = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await service[methodName](filters, options)
|
let result: any
|
||||||
|
if (RemoteQuery.traceFetchRemoteData) {
|
||||||
|
result = await RemoteQuery.traceFetchRemoteData(
|
||||||
|
async () => service[methodName](filters, options),
|
||||||
|
serviceConfig.serviceName,
|
||||||
|
methodName,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
result = await service[methodName](filters, options)
|
||||||
|
}
|
||||||
|
|
||||||
if (hasPagination) {
|
if (hasPagination) {
|
||||||
const [data, count] = result
|
const [data, count] = result
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ export * from "./telemetry"
|
|||||||
|
|
||||||
export const MEDUSA_CLI_PATH = require.resolve("@medusajs/medusa-cli")
|
export const MEDUSA_CLI_PATH = require.resolve("@medusajs/medusa-cli")
|
||||||
|
|
||||||
export { GraphQLSchema, gqlSchemaToTypes } from "@medusajs/modules-sdk"
|
export { GraphQLSchema, gqlSchemaToTypes, Query } from "@medusajs/modules-sdk"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { snakeCase } from "lodash"
|
|||||||
import { NodeSDK } from "@opentelemetry/sdk-node"
|
import { NodeSDK } from "@opentelemetry/sdk-node"
|
||||||
import { Resource } from "@opentelemetry/resources"
|
import { Resource } from "@opentelemetry/resources"
|
||||||
import { SpanStatusCode } from "@opentelemetry/api"
|
import { SpanStatusCode } from "@opentelemetry/api"
|
||||||
import { RoutesLoader, Tracer } from "@medusajs/framework"
|
import { RoutesLoader, Tracer, Query } from "@medusajs/framework"
|
||||||
import {
|
import {
|
||||||
type SpanExporter,
|
type SpanExporter,
|
||||||
SimpleSpanProcessor,
|
SimpleSpanProcessor,
|
||||||
@@ -21,7 +21,7 @@ function shouldExcludeResource(resource: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumenting the first touch point of the Http layer to report traces to
|
* Instrumenting the first touch point of the HTTP layer to report traces to
|
||||||
* OpenTelemetry
|
* OpenTelemetry
|
||||||
*/
|
*/
|
||||||
export function instrumentHttpLayer() {
|
export function instrumentHttpLayer() {
|
||||||
@@ -43,6 +43,12 @@ export function instrumentHttpLayer() {
|
|||||||
try {
|
try {
|
||||||
await requestHandler()
|
await requestHandler()
|
||||||
} finally {
|
} finally {
|
||||||
|
if (res.statusCode >= 500) {
|
||||||
|
span.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: `Failed with ${res.statusMessage}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
span.setAttributes({ "http.statusCode": res.statusCode })
|
span.setAttributes({ "http.statusCode": res.statusCode })
|
||||||
span.end()
|
span.end()
|
||||||
}
|
}
|
||||||
@@ -71,6 +77,7 @@ export function instrumentHttpLayer() {
|
|||||||
code: SpanStatusCode.ERROR,
|
code: SpanStatusCode.ERROR,
|
||||||
message: error.message || "Failed",
|
message: error.message || "Failed",
|
||||||
})
|
})
|
||||||
|
throw error
|
||||||
} finally {
|
} finally {
|
||||||
span.end()
|
span.end()
|
||||||
}
|
}
|
||||||
@@ -117,6 +124,85 @@ export function instrumentHttpLayer() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumenting the queries made using the remote query
|
||||||
|
*/
|
||||||
|
export function instrumentRemoteQuery() {
|
||||||
|
const QueryTracer = new Tracer("@medusajs/query", "2.0.0")
|
||||||
|
|
||||||
|
Query.instrument.graphQuery(async function (queryFn, queryOptions) {
|
||||||
|
return await QueryTracer.trace(
|
||||||
|
`query.graph: ${queryOptions.entryPoint}`,
|
||||||
|
async (span) => {
|
||||||
|
span.setAttributes({
|
||||||
|
"query.fields": queryOptions.fields,
|
||||||
|
})
|
||||||
|
return await queryFn()
|
||||||
|
.catch((error) => {
|
||||||
|
span.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => span.end())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
Query.instrument.remoteQuery(async function (queryFn, queryOptions) {
|
||||||
|
const traceIdentifier =
|
||||||
|
"entryPoint" in queryOptions
|
||||||
|
? queryOptions.entryPoint
|
||||||
|
: "service" in queryOptions
|
||||||
|
? queryOptions.service
|
||||||
|
: "__value" in queryOptions
|
||||||
|
? Object.keys(queryOptions.__value)[0]
|
||||||
|
: "unknown source"
|
||||||
|
|
||||||
|
return await QueryTracer.trace(
|
||||||
|
`remoteQuery: ${traceIdentifier}`,
|
||||||
|
async (span) => {
|
||||||
|
span.setAttributes({
|
||||||
|
"query.fields": "fields" in queryOptions ? queryOptions.fields : [],
|
||||||
|
})
|
||||||
|
return await queryFn()
|
||||||
|
.catch((error) => {
|
||||||
|
span.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => span.end())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
Query.instrument.remoteDataFetch(async function (
|
||||||
|
fetchFn,
|
||||||
|
serviceName,
|
||||||
|
method,
|
||||||
|
options
|
||||||
|
) {
|
||||||
|
return await QueryTracer.trace(
|
||||||
|
`${snakeCase(serviceName)}.${snakeCase(method)}`,
|
||||||
|
async (span) => {
|
||||||
|
span.setAttributes({
|
||||||
|
"fetch.select": options.select,
|
||||||
|
"fetch.relations": options.relations,
|
||||||
|
})
|
||||||
|
return await fetchFn()
|
||||||
|
.catch((error) => {
|
||||||
|
span.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => span.end())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper function to configure the OpenTelemetry SDK with some defaults.
|
* A helper function to configure the OpenTelemetry SDK with some defaults.
|
||||||
* For better/more control, please configure the SDK manually.
|
* For better/more control, please configure the SDK manually.
|
||||||
|
|||||||
Reference in New Issue
Block a user