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

feat: [plugin/prometheus] allow to skip observation based on context #2317

Open
wants to merge 6 commits into
base: main
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
73 changes: 73 additions & 0 deletions .changeset/cold-seals-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
'@envelop/prometheus': major
---

Allow to explicitly control which events and timing should be observe.

Each metric can now be configured to observe events and timings only for certain GraphQL pipeline
phases, or depending on the request context.

```ts
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { usePrometheus } from '@envelop/prometheus'

const TRACKED_OPERATION_NAMES = [
// make a list of operation that you want to monitor
]

const getEnveloped = envelop({
plugins: [
useEngine({ parse, validate, specifiedRules, execute, subscribe }),
usePrometheus({
metrics: {

// only trace errors of execute and subscribe phases
graphql_envelop_phase_error: ['execute', 'subscribe']

// only monitor timing of some operations
graphql_yoga_http_duration: createHistogram({
registry,
histogram: {
name: 'graphql_yoga_http_duration',
help: 'Time spent on HTTP connection',
labelNames: ['operation_name']
},
fillLabelsFn: ({ operationName }, _rawContext) => ({
operation_name: operationName,
}),

// Here `shouldObserve` control if the request timing should be observed, based on context
shouldObserve: context => TRACKED_OPERATIONS.includes(context?.params?.operationName),
})
},
})
]
})
```

**Breaking Change**: A metric is enabled using `true` value in metrics options will observe in every
phases available.

Previously, which phase was observe was depending on which other metric were enabled. For example,
this config would only trace validation error:

```ts
usePrometheus({
metrics: {
graphql_envelop_phase_error: true,
graphql_envelop_phase_validate: true,
},
})
```

This is no longer the case. If you were relying on this behavior, please use an array of string to
restrict observed phases.

```ts
usePrometheus({
metrics: {
graphql_envelop_phase_error: ['validate'],
},
})
```
73 changes: 73 additions & 0 deletions packages/plugins/prometheus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ Histogram metrics can be passed an array of numbers to configure buckets.
Each metric also expose a set of labels. All labels are exposed by default but can be separately
disabled by setting the corresponding key in `labels` option object to `false`.

A metric can observe events in different phases of of GraphQL request pipeline. By default, if a
metric is available, it will observe timing or events in every available phases for this metric. You
can configure this by either providing an array instead of `true` in the metrics config, or use the
`phases` option in the custom metric factory.

### `graphql_envelop_phase_parse`

This metric tracks the duration of the `parse` phase of the GraphQL execution. It reports the time
Expand Down Expand Up @@ -519,6 +524,7 @@ const getEnveloped = envelop({
help: 'HELP ME',
labelNames: ['opText'] as const,
}),
phases: ['parse'], // This is an array of phases that should be hooked for this metric
fillLabelsFn: params => {
// if you wish to fill your `labels` with metadata, you can use the params in order to get access to things like DocumentNode, operationName, operationType, `error` (for error metrics) and `info` (for resolvers metrics)
return {
Expand All @@ -532,6 +538,73 @@ const getEnveloped = envelop({
})
```

### Configure metric phases

Each metric observes timing or events in different phases of the GraphQL request pipeline.

You can configure which phases are observed for a given metric by providing an array of phases
instead of `true` for any metric configuration. You can also configure the phases when using custom
metrics factories by providing the `phases` option.

By default, all available phases are enabled when the metric is enabled.

```ts
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { usePrometheus } from '@envelop/prometheus'

const getEnveloped = envelop({
plugins: [
useEngine({ parse, validate, specifiedRules, execute, subscribe }),
usePrometheus({
metrics: {
graphql_envelop_phase_error: ['execute', 'subscribe'] // only trace errors of execute and subscribe phases
}
})
]
})
```

### Skip observation based on request context

To save bandwidth or storage, you can reduce the amount of reported values by filtering which events
are observed based on the request context.

For example, you can only monitor a subset of operations, because they are critical or that you want
to debug it's performance:

```ts
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { usePrometheus } from '@envelop/prometheus'

const TRACKED_OPERATION_NAMES = [
// make a list of operation that you want to monitor
]

const getEnveloped = envelop({
plugins: [
useEngine({ parse, validate, specifiedRules, execute, subscribe }),
usePrometheus({
metrics: {
graphql_yoga_http_duration: createHistogram({
registry,
histogram: {
name: 'graphql_yoga_http_duration',
help: 'Time spent on HTTP connection',
labelNames: ['operation_name']
},
fillLabelsFn: ({ operationName }, _rawContext) => ({
operation_name: operationName
}),
shouldObserve: context => TRACKED_OPERATIONS.includes(context?.params?.operationName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it!

But I think the example should be updated:

Suggested change
shouldObserve: context => TRACKED_OPERATIONS.includes(context?.params?.operationName)
shouldObserve: (_, context) => TRACKED_OPERATIONS.includes(context?.params?.operationName)

because I think the signature is the same as fillLabelsFn.

})
}
})
]
})
```

## Caveats

Due to Prometheus client API limitations, if this plugin is initialized multiple times, only the
Expand Down
97 changes: 72 additions & 25 deletions packages/plugins/prometheus/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Registry } from 'prom-client';
import { createCounter, createHistogram, createSummary } from './utils.js';
import { createCounter, createHistogram, createSummary, type AtLeastOne } from './utils.js';

export type PrometheusTracingPluginConfig = {
/**
Expand Down Expand Up @@ -47,8 +47,14 @@ export type MetricsConfig = {
* Tracks the number of GraphQL operations executed.
* It counts all operations, either failed or successful, including subscriptions.
* It is exposed as a counter.
*
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - ReturnType<typeof createCounter>: Enable the metric with custom configuration
*/
graphql_envelop_request?: boolean | string | ReturnType<typeof createCounter>;
graphql_envelop_request?: CounterMetricOption<AtLeastOne<'execute' | 'subscribe'>>;

/**
* Tracks the duration of the complete GraphQL operation execution.
Expand All @@ -57,19 +63,22 @@ export type MetricsConfig = {
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - number[]: Enable the metric with custom buckets
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration
*/
graphql_envelop_request_duration?:
| boolean
| string
| number[]
| ReturnType<typeof createHistogram>;
graphql_envelop_request_duration?: HistogramMetricOption<AtLeastOne<'execute' | 'subscribe'>>;
/**
* Provides a summary of the time spent on the GraphQL operation execution.
* It reports the same timing than graphql_envelop_request_duration but as a summary.
*
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - ReturnType<typeof createSummary>: Enable the metric with custom configuration
*/
graphql_envelop_request_time_summary?: boolean | string | ReturnType<typeof createSummary>;
graphql_envelop_request_time_summary?: SummaryMetricOption<AtLeastOne<'execute' | 'subscribe'>>;
/**
* Tracks the duration of the parse phase of the GraphQL execution.
* It reports the time spent parsing the incoming GraphQL operation.
Expand All @@ -78,10 +87,11 @@ export type MetricsConfig = {
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - number[]: Enable the metric with custom buckets
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration
*/
graphql_envelop_phase_parse?: boolean | string | number[] | ReturnType<typeof createHistogram>;
graphql_envelop_phase_parse?: HistogramMetricOption<['parse']>;
/**
* Tracks the duration of the validate phase of the GraphQL execution.
* It reports the time spent validating the incoming GraphQL operation.
Expand All @@ -90,10 +100,11 @@ export type MetricsConfig = {
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - number[]: Enable the metric with custom buckets
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration
*/
graphql_envelop_phase_validate?: boolean | string | number[] | ReturnType<typeof createHistogram>;
graphql_envelop_phase_validate?: HistogramMetricOption<['validate']>;
/**
* Tracks the duration of the context phase of the GraphQL execution.
* It reports the time spent building the context object that will be passed to the executors.
Expand All @@ -102,10 +113,11 @@ export type MetricsConfig = {
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - number[]: Enable the metric with custom buckets
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration
*/
graphql_envelop_phase_context?: boolean | string | number[] | ReturnType<typeof createHistogram>;
graphql_envelop_phase_context?: HistogramMetricOption<['context']>;
/**
* Tracks the duration of the execute phase of the GraphQL execution.
* It reports the time spent actually resolving the response of the incoming operation.
Expand All @@ -115,10 +127,11 @@ export type MetricsConfig = {
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - number[]: Enable the metric with custom buckets
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration
*/
graphql_envelop_phase_execute?: boolean | string | number[] | ReturnType<typeof createHistogram>;
graphql_envelop_phase_execute?: HistogramMetricOption<['execute']>;
/**
* This metric tracks the duration of the subscribe phase of the GraphQL execution.
* It reports the time spent initiating a subscription (which doesn’t include actually sending the first response).
Expand All @@ -127,34 +140,51 @@ export type MetricsConfig = {
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - number[]: Enable the metric with custom buckets
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration
*/
graphql_envelop_phase_subscribe?:
| boolean
| string
| number[]
| ReturnType<typeof createHistogram>;
graphql_envelop_phase_subscribe?: HistogramMetricOption<['subscribe']>;
/**
* This metric tracks the number of errors that returned by the GraphQL execution.
* It counts all errors found in response, but it also includes errors from other GraphQL
* processing phases (parsing, validation and context building).
* It is exposed as a counter.
*
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - ReturnType<typeof createCounter>: Enable the metric with custom configuration
*/
graphql_envelop_error_result?: boolean | string | ReturnType<typeof createCounter>;
graphql_envelop_error_result?: CounterMetricOption<
AtLeastOne<'parse' | 'validate' | 'context' | 'execute' | 'subscribe'>
>;
/**
* This metric tracks the number of deprecated fields used in the GraphQL operation.
* It is exposed as a counter.
*
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - ReturnType<typeof createCounter>: Enable the metric with custom configuration
*/
graphql_envelop_deprecated_field?: boolean | string | ReturnType<typeof createCounter>;
graphql_envelop_deprecated_field?: CounterMetricOption<['parse']>;
/**
* This metric tracks the number of schema changes that have occurred since the gateway started.
* If you are using a plugin that modifies the schema on the fly,
* be aware that this metric will also include updates made by those plugins.
* Which means that one schema update can actually trigger multiple schema changes.
* It is exposed as a counter.
*
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - ReturnType<typeof createCounter>: Enable the metric with custom configuration
*/
graphql_envelop_schema_change?: boolean | string | ReturnType<typeof createCounter>;
graphql_envelop_schema_change?: CounterMetricOption<['schema']>;
/**
* This metric tracks the duration of each resolver execution.
*
Expand All @@ -165,14 +195,11 @@ export type MetricsConfig = {
* You can pass multiple type of values:
* - boolean: Disable or Enable the metric with default configuration
* - string: Enable the metric with custom name
* - string[]: Enable the metric on a list of phases
* - number[]: Enable the metric with custom buckets
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration
*/
graphql_envelop_execute_resolver?:
| boolean
| string
| number[]
| ReturnType<typeof createHistogram>;
graphql_envelop_execute_resolver?: HistogramMetricOption<AtLeastOne<'subscribe' | 'execute'>>;
};

export type LabelsConfig = {
Expand Down Expand Up @@ -219,3 +246,23 @@ export type LabelsConfig = {
*/
phase?: boolean;
};

export type HistogramMetricOption<Phases extends string[], LabelNames extends string = string> =
| boolean
| string
| BucketsConfig
| Phases
| ReturnType<typeof createHistogram<Phases, LabelNames>>;
export type BucketsConfig = AtLeastOne<number>;

export type CounterMetricOption<Phases extends string[], LabelNames extends string = string> =
| boolean
| string
| Phases
| ReturnType<typeof createCounter<Phases, LabelNames>>;

export type SummaryMetricOption<Phases extends string[], LabelNames extends string = string> =
| boolean
| string
| Phases
| ReturnType<typeof createSummary<Phases, LabelNames>>;
Loading
Loading