From f915c7d1a2e9d17337de9462e41da9777e15bd8a Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Wed, 13 Sep 2023 20:40:54 +0700 Subject: [PATCH] server: support w3c traceparent context PR-URL: https://github.com/hasura/graphql-engine-mono/pull/10218 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rob Dominguez <24390149+robertjdominguez@users.noreply.github.com> GitOrigin-RevId: d3dbea6220fd2127ab76c0a240fc4725ca5d6aac --- .../metadata-api/observability.mdx | 1 + docs/docs/api-reference/syntax-defs.mdx | 311 ++++++++---------- docs/docs/observability/opentelemetry.mdx | 23 ++ .../open-telemetry-trace-propagation.png | Bin 0 -> 97089 bytes .../OpenTelemetry/OpenTelemetry.stories.tsx | 3 + .../OpenTelemetry/components/Form/Form.tsx | 23 +- .../OpenTelemetry/components/Form/schema.ts | 6 + .../OpenTelemetryFeature.stories.tsx | 2 + .../utils/openTelemetryToFormValues.ts | 5 +- .../openTelemetry/types.ts | 5 + metadata.openapi.json | 15 + server/graphql-engine.cabal | 5 + .../Backends/DataConnector/Agent/Client.hs | 4 +- .../Backends/Postgres/Execute/Mutation.hs | 3 +- .../src-lib/Hasura/Eventing/EventTrigger.hs | 6 +- server/src-lib/Hasura/Eventing/HTTP.hs | 5 +- .../Hasura/Eventing/ScheduledTrigger.hs | 22 +- server/src-lib/Hasura/GraphQL/Execute.hs | 4 + .../src-lib/Hasura/GraphQL/Execute/Action.hs | 22 +- .../Hasura/GraphQL/Execute/Mutation.hs | 9 +- .../src-lib/Hasura/GraphQL/Execute/Query.hs | 3 + .../Hasura/GraphQL/Execute/RemoteJoin/Join.hs | 5 +- server/src-lib/Hasura/GraphQL/RemoteServer.hs | 7 +- .../src-lib/Hasura/GraphQL/Transport/HTTP.hs | 14 +- .../Hasura/GraphQL/Transport/WebSocket.hs | 22 +- .../src-lib/Hasura/RQL/DDL/OpenTelemetry.hs | 6 +- .../src-lib/Hasura/RQL/Types/OpenTelemetry.hs | 67 +++- server/src-lib/Hasura/Server/App.hs | 29 +- server/src-lib/Hasura/Tracing.hs | 4 + server/src-lib/Hasura/Tracing/Class.hs | 3 +- server/src-lib/Hasura/Tracing/Context.hs | 6 +- server/src-lib/Hasura/Tracing/Propagator.hs | 64 ++++ .../src-lib/Hasura/Tracing/Propagator/B3.hs | 49 +++ .../Tracing/Propagator/W3CTraceContext.hs | 129 ++++++++ server/src-lib/Hasura/Tracing/Sampling.hs | 1 + server/src-lib/Hasura/Tracing/TraceState.hs | 54 +++ server/src-lib/Hasura/Tracing/Utils.hs | 14 +- .../src-test/Hasura/Tracing/PropagatorSpec.hs | 80 +++++ 38 files changed, 774 insertions(+), 257 deletions(-) create mode 100644 docs/static/img/enterprise/open-telemetry-trace-propagation.png create mode 100644 server/src-lib/Hasura/Tracing/Propagator.hs create mode 100644 server/src-lib/Hasura/Tracing/Propagator/B3.hs create mode 100644 server/src-lib/Hasura/Tracing/Propagator/W3CTraceContext.hs create mode 100644 server/src-lib/Hasura/Tracing/TraceState.hs create mode 100644 server/src-test/Hasura/Tracing/PropagatorSpec.hs diff --git a/docs/docs/api-reference/metadata-api/observability.mdx b/docs/docs/api-reference/metadata-api/observability.mdx index d5d0a4dae9b..06a8c486eaa 100644 --- a/docs/docs/api-reference/metadata-api/observability.mdx +++ b/docs/docs/api-reference/metadata-api/observability.mdx @@ -108,6 +108,7 @@ X-Hasura-Role: admin "otlp_traces_endpoint": "http://localhost:4318/v1/traces", "otlp_metrics_endpoint": "http://localhost:4318/v1/metrics", "protocol": "http/protobuf", + "traces_propagators": ["tracecontext"], "headers": [ { "name": "x-test-header", diff --git a/docs/docs/api-reference/syntax-defs.mdx b/docs/docs/api-reference/syntax-defs.mdx index d0fd947a12d..3fa81d269cd 100644 --- a/docs/docs/api-reference/syntax-defs.mdx +++ b/docs/docs/api-reference/syntax-defs.mdx @@ -92,13 +92,13 @@ keywords: ## PGConfiguration {#pgconfiguration} -| Key | Required | Schema | Description | -| ------------------- | -------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | -| connection_info | true | [PGSourceConnectionInfo](#pgsourceconnectioninfo) | Connection parameters for the source | -| read_replicas | false | \[[PGSourceConnectionInfo](#pgsourceconnectioninfo)\] | Optional list of read replica configuration *(supported only in cloud/enterprise versions)* | -| extensions_schema | false | String | Name of the schema where the graphql-engine will install database extensions (default: `public`) | -| connection_template | false | [PGConnectionTemplate](#pgconnectiontemplate) | DB connection template *(supported only in cloud/enterprise versions)* | -| connection_set | false | \[[ConnectionSetElementConfig](#connectionsetelementconfig)\] | Connection Set used for DB connection template*(supported only in cloud/enterprise versions)* | +| Key | Required | Schema | Description | +| ------------------- | -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| connection_info | true | [PGSourceConnectionInfo](#pgsourceconnectioninfo) | Connection parameters for the source | +| read_replicas | false | \[[PGSourceConnectionInfo](#pgsourceconnectioninfo)\] | Optional list of read replica configuration _(supported only in cloud/enterprise versions)_ | +| extensions_schema | false | String | Name of the schema where the graphql-engine will install database extensions (default: `public`) | +| connection_template | false | [PGConnectionTemplate](#pgconnectiontemplate) | DB connection template _(supported only in cloud/enterprise versions)_ | +| connection_set | false | \[[ConnectionSetElementConfig](#connectionsetelementconfig)\] | Connection Set used for DB connection template*(supported only in cloud/enterprise versions)* | ## MsSQLConfiguration {#mssqlconfiguration} @@ -130,19 +130,19 @@ keywords: :::info Note When `use_prepared_statements` is `true`, all SQL queries compiled from GraphQL queries will be -[prepared](https://www.postgresql.org/docs/current/sql-prepare.html) before being executed, meaning that the -database server will cache queries and query plans. +[prepared](https://www.postgresql.org/docs/current/sql-prepare.html) before being executed, meaning that the database +server will cache queries and query plans. -This can result in an improvement in performance when serving mostly complex queries with little variation. But it's -a trade-off that increases memory usage, and under other circumstances the result is not a net performance gain. -And because the prepared statements cache is local to each database connection, the connection pool parameters also +This can result in an improvement in performance when serving mostly complex queries with little variation. But it's a +trade-off that increases memory usage, and under other circumstances the result is not a net performance gain. And +because the prepared statements cache is local to each database connection, the connection pool parameters also influence its efficiency. -The only way to reasonably know if enabling prepared statements will increase the performance of a Hasura GraphQL -Engine instance is to benchmark it under a representative query load. +The only way to reasonably know if enabling prepared statements will increase the performance of a Hasura GraphQL Engine +instance is to benchmark it under a representative query load. -This option interacts with the [Query Tags](/observability/query-tags.mdx) feature (see for details), -and the two generally shouldn't be enabled at the same time. +This option interacts with the [Query Tags](/observability/query-tags.mdx) feature (see for details), and the two +generally shouldn't be enabled at the same time. ::: @@ -177,7 +177,7 @@ and the two generally shouldn't be enabled at the same time. | max_connections | false | `Integer` | Maximum number of connections to be kept in the pool (default: 50) | | total_max_connections | false | `Integer` | Maximum number of total connections to be maintained across any number of Hasura Cloud instances (default: 1000). Takes precedence over `max_connections` in Cloud projects. _(Only available in Hasura Cloud)_ | | idle_timeout | false | `Integer` | The idle timeout (in seconds) per connection (default: 180) | -| retries | false | `Integer` | Number of retries to perform when failing to acquire connection (default: 1). Note that this configuration does not affect user/statement errors on PG. | +| retries | false | `Integer` | Number of retries to perform when failing to acquire connection (default: 1). Note that this configuration does not affect user/statement errors on PG. | | pool_timeout | false | `Integer` | Maximum time to wait while acquiring a Postgres connection from the pool, in seconds (default: forever) | | connection_lifetime | false | `Integer` | Time from connection creation after which the connection should be destroyed and a new one created. A value of 0 indicates we should never destroy an active connection. If 0 is passed, memory from large query results may not be reclaimed. (default: 600 sec) | @@ -205,9 +205,9 @@ This schema indicates that the source uses a connection pool (the default): This schema indicates that the source does not use a connection pool: -| Key | Required | Schema | Description | -| -------------- | -------- | --------- | ------------------------------------------------------ | -| enable | true | `Bool` | Set to `false` to disable the connection pool entirely | +| Key | Required | Schema | Description | +| ------ | -------- | ------ | ------------------------------------------------------ | +| enable | true | `Bool` | Set to `false` to disable the connection pool entirely | ## PGColumnType {#pgcolumntype} @@ -352,7 +352,7 @@ Configuration properties for particular column, as specified on [ColumnConfig](# ## InputValidationDefinition {#input-validation-definition} | Key | Required | Schema | Description | -| ----------- | -------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| ---------------------- | -------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | url | true | [WebhookURL](#webhookurl) | The input validations's webhook URL | | headers | false | \[[HeaderFromValue](#headerfromvalue) \| [HeaderFromEnv](#headerfromenv) \] | List of defined headers to be sent to the handler | | forward_client_headers | false | boolean | If set to `true` the client headers are forwarded to the webhook handler (default: `false`) | @@ -360,15 +360,15 @@ Configuration properties for particular column, as specified on [ColumnConfig](# ## InputValidation {#input-validation} -| Key | Required | Schema | Description | -| ---------------------- | -------- | -------- | -------------------------------------------------------------------- | -| type | true | `String` | The interface for input validation. (Currently only supports "http") | -| definition | true | [InputValidationDefinition](#input-validation-definition) | The definition for the input validation | +| Key | Required | Schema | Description | +| ---------- | -------- | --------------------------------------------------------- | -------------------------------------------------------------------- | +| type | true | `String` | The interface for input validation. (Currently only supports "http") | +| definition | true | [InputValidationDefinition](#input-validation-definition) | The definition for the input validation | ## InsertPermission {#insertpermission} | Key | Required | Schema | Description | -| ------------ | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| -------------- | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | check | true | [BoolExp](#boolexp) | This expression has to hold true for every new row that is inserted | | set | false | [ColumnPresetsExp](#columnpresetexp) | Preset values for columns that can be sourced from session variables or static values | | columns | false | [PGColumn](#pgcolumn) array (or) `'*'` | Can insert into only these columns (or all when `'*'` is specified) | @@ -396,7 +396,7 @@ The `query_root_fields` and the `subscription_root_fields` are only available in ## UpdatePermission {#updatepermission} | Key | Required | Schema | Description | -| ------------ | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| -------------- | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | columns | true | [PGColumn](#pgcolumn) array (or) `'*'` | Only these columns are selectable (or all when `'*'` is specified) | | filter | true | [BoolExp](#boolexp) | Only the rows where this precondition holds true are updatable | | check | false | [BoolExp](#boolexp) | Postcondition which must be satisfied by rows which have been updated | @@ -404,21 +404,20 @@ The `query_root_fields` and the `subscription_root_fields` are only available in | backend_only | false | Boolean | When set to `true` the mutation is accessible only if the `x-hasura-use-backend-only-permissions` session variable exists and is set to `true` and the request is made with `x-hasura-admin-secret` set if any auth is configured | | validate_input | false | [InputValidation](#input-validation) | The input validation definition for the insert mutation. | - ## DeletePermission {#deletepermission} -| Key | Required | Schema | Description | -| ------------ | -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| filter | true | [BoolExp](#boolexp) | Only the rows where this expression holds true are deletable | -| backend_only | false | Boolean | When set to `true` the mutation is accessible only if the `x-hasura-use-backend-only-permissions` session variable exists and is set to `true` and the request is made with `x-hasura-admin-secret` set if any auth is configured | -| validate_input | false | [InputValidation](#input-validation) | The input validation definition for the insert mutation. | +| Key | Required | Schema | Description | +| -------------- | -------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| filter | true | [BoolExp](#boolexp) | Only the rows where this expression holds true are deletable | +| backend_only | false | Boolean | When set to `true` the mutation is accessible only if the `x-hasura-use-backend-only-permissions` session variable exists and is set to `true` and the request is made with `x-hasura-admin-secret` set if any auth is configured | +| validate_input | false | [InputValidation](#input-validation) | The input validation definition for the insert mutation. | ## LogicalModelSelectPermission {#logicalmodelselectpermission} -| Key | Required | Schema | Description | -| ------------------------ | -------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| columns | true | [PGColumn](#pgcolumn) array (or) `'*'` | Only these columns are selectable (or all when `'*'` is specified) | -| filter | true | [BoolExp](#boolexp) | Only the rows where this expression holds true are selectable | +| Key | Required | Schema | Description | +| ------- | -------- | -------------------------------------- | ------------------------------------------------------------------ | +| columns | true | [PGColumn](#pgcolumn) array (or) `'*'` | Only these columns are selectable (or all when `'*'` is specified) | +| filter | true | [BoolExp](#boolexp) | Only the rows where this expression holds true are selectable | ## ObjRelUsing {#objrelusing} @@ -429,8 +428,7 @@ The `query_root_fields` and the `subscription_root_fields` are only available in :::info Note -There has to be at least one and only one of `foreign_key_constraint_on` -and `manual_configuration`. +There has to be at least one and only one of `foreign_key_constraint_on` and `manual_configuration`. ::: @@ -499,9 +497,8 @@ Supported in `v2.0.0-alpha.3` and above. ## InsertOrder {#insertorder} -Describes when should the referenced table row be inserted in relation -to the current table row in case of a nested insert. Defaults to -"before_parent". +Describes when should the referenced table row be inserted in relation to the current table row in case of a nested +insert. Defaults to "before_parent". ``` "before_parent" | "after_parent" @@ -677,10 +674,9 @@ scheduled | locked | delivered | error | dead | `"$in"` | `IN` | | `"$nin"` | `NOT IN` | -(For more details, refer to the Postgres docs for [comparison -operators](https://www.postgresql.org/docs/current/functions-comparison.html) -and [list based search -operators](https://www.postgresql.org/docs/current/functions-comparisons.html).) +(For more details, refer to the Postgres docs for +[comparison operators](https://www.postgresql.org/docs/current/functions-comparison.html) and +[list based search operators](https://www.postgresql.org/docs/current/functions-comparisons.html).) **Text related operators :** @@ -697,11 +693,10 @@ operators](https://www.postgresql.org/docs/current/functions-comparisons.html).) | `$nregex` | `!~` | | `$niregex` | `!~*` | -(For more details on text related operators, refer to the [Postgres -docs](https://www.postgresql.org/docs/current/functions-matching.html).) +(For more details on text related operators, refer to the +[Postgres docs](https://www.postgresql.org/docs/current/functions-matching.html).) -**Operators for comparing columns (all column types except json, -jsonb):** +**Operators for comparing columns (all column types except json, jsonb):** **Column Comparison Operator** @@ -729,9 +724,8 @@ jsonb):** -Column comparison operators can be used to compare columns of the same -table or a related table. To compare a column of a table with another -column of : +Column comparison operators can be used to compare columns of the same table or a related table. To compare a column of +a table with another column of : 1. The same table - @@ -790,8 +784,8 @@ column of : | `"$cgte"` | `>=` | | `"$clte"` | `<=` | -(For more details on comparison operators, refer to the [Postgres -docs](https://www.postgresql.org/docs/current/functions-comparison.html).) +(For more details on comparison operators, refer to the +[Postgres docs](https://www.postgresql.org/docs/current/functions-comparison.html).) **Checking for NULL values :** @@ -799,8 +793,8 @@ docs](https://www.postgresql.org/docs/current/functions-comparison.html).) | --------------------------------------- | --------------------- | | `_is_null` (takes true/false as values) | `IS NULL` | -(For more details on the `IS NULL` expression, refer to the [Postgres -docs](https://www.postgresql.org/docs/current/functions-comparison.html).) +(For more details on the `IS NULL` expression, refer to the +[Postgres docs](https://www.postgresql.org/docs/current/functions-comparison.html).) **JSONB operators :** @@ -812,8 +806,8 @@ docs](https://www.postgresql.org/docs/current/functions-comparison.html).) | `_has_keys_any` | `?!` | | `_has_keys_all` | `?&` | -(For more details on JSONB operators, refer to the [Postgres -docs](https://www.postgresql.org/docs/current/static/functions-json.html#FUNCTIONS-JSONB-OP-TABLE).) +(For more details on JSONB operators, refer to the +[Postgres docs](https://www.postgresql.org/docs/current/static/functions-json.html#FUNCTIONS-JSONB-OP-TABLE).) **PostGIS related operators on GEOMETRY columns:** @@ -831,13 +825,11 @@ docs](https://www.postgresql.org/docs/current/static/functions-json.html#FUNCTIO | `_st_3d_d_within` | `ST_3DDWithin(column, input)` | (For more details on spatial relationship operators, refer to the -[PostGIS -docs](http://postgis.net/workshops/postgis-intro/spatial_relationships.html).) +[PostGIS docs](http://postgis.net/workshops/postgis-intro/spatial_relationships.html).) :::info Note -- All operators take a JSON representation of `geometry/geography` - values as input value. +- All operators take a JSON representation of `geometry/geography` values as input value. - The input value for `_st_d_within` operator is an object: @@ -871,9 +863,8 @@ An empty [JSONObject](https://tools.ietf.org/html/rfc7159) ## ColumnPresetsExp {#columnpresetexp} -A [JSONObject](https://tools.ietf.org/html/rfc7159) of a Postgres column -name to value mapping, where the value can be static or derived from a -session variable. +A [JSONObject](https://tools.ietf.org/html/rfc7159) of a Postgres column name to value mapping, where the value can be +static or derived from a session variable. ``` { @@ -883,8 +874,7 @@ session variable. } ``` -E.g. where `id` is derived from a session variable and `city` is a -static value. +E.g. where `id` is derived from a session variable and `city` is a static value. ```json { @@ -895,9 +885,8 @@ static value. :::info Note -If the value of any key begins with "x-hasura-" (_case-insensitive_), -the value of the column specified in the key will be derived from a -session variable of the same name. +If the value of any key begins with "x-hasura-" (_case-insensitive_), the value of the column specified in the key will +be derived from a session variable of the same name. ::: @@ -1011,12 +1000,10 @@ session variable of the same name. | `suffix` | false | String | Suffix applied to type names in the Remote Schema | | `mapping` | false | `{String: String}` | Explicit mapping of type names in the Remote Schema Note: explicit mapping takes precedence over `prefix` and `suffix`. | -- Type name prefix and suffix will be applied to all types in the - schema except the root types (for query, mutation and subscription), - types starting with `__`, standard scalar types (`Int`, `Float`, - `String`, `Boolean`, and `ID`), and types with an explicit mapping. -- Root types, types starting with `__`, and standard scalar types may - only be customized with an explicit mapping. +- Type name prefix and suffix will be applied to all types in the schema except the root types (for query, mutation and + subscription), types starting with `__`, standard scalar types (`Int`, `Float`, `String`, `Boolean`, and `ID`), and + types with an explicit mapping. +- Root types, types starting with `__`, and standard scalar types may only be customized with an explicit mapping. ## RemoteFieldCustomization {#remotefieldcustomization} @@ -1027,8 +1014,8 @@ session variable of the same name. | `suffix` | false | String | Suffix applied to field names in the parent type | | `mapping` | false | `{String: String}` | Explicit mapping of field names in the parent type Note: explicit mapping takes precedence over `prefix` and `suffix`. | -- Fields that are part of an interface must be renamed consistently - across all object types that implement that interface. +- Fields that are part of an interface must be renamed consistently across all object types that implement that + interface. ## SourceCustomization {#sourcecustomization} @@ -1055,22 +1042,21 @@ session variable of the same name. :::info Note -Please note that the naming convention feature is an experimental feature for now. -To use this feature, please use the `--experimental-features=naming_convention` -flag or set the `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` environment variable to -`naming_convention`. +Please note that the naming convention feature is an experimental feature for now. To use this feature, please use the +`--experimental-features=naming_convention` flag or set the `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` environment variable +to `naming_convention`. -The naming convention can either be `graphql-default` or `hasura-default` (default). -**The `graphql-default` naming convention is supported only for postgres databases right now.** -Typecase for each of the naming convention is mentioned below: +The naming convention can either be `graphql-default` or `hasura-default` (default). **The `graphql-default` naming +convention is supported only for postgres databases right now.** Typecase for each of the naming convention is mentioned +below: | Naming Convention | Field names | Type names | Arguments | Enum values | | ----------------- | ----------- | ----------- | ---------- | ----------- | | hasura-default | Snake case | Snake case | Snake case | as defined | | graphql-default | Camel case | Pascal case | Camel case | Uppercased | -The naming convention can be overridden by `custom_name` in [Table Config](#table-config) -or by setting [Custom Root Fields](#custom-root-fields). +The naming convention can be overridden by `custom_name` in [Table Config](#table-config) or by setting +[Custom Root Fields](#custom-root-fields). ::: @@ -1174,14 +1160,14 @@ or by setting [Custom Root Fields](#custom-root-fields). :::caution Deprecation -CustomColumnNames is deprecated in favour of using the `custom_name` property on columns in [ColumnConfig](#columnconfig). -If both CustomColumnNames and [ColumnConfig](#columnconfig) is used, any `custom_name ` properties used in -[ColumnConfig](#columnconfig) will take precedence and any overlapped values in `custom_column_names` will be discarded. +CustomColumnNames is deprecated in favour of using the `custom_name` property on columns in +[ColumnConfig](#columnconfig). If both CustomColumnNames and [ColumnConfig](#columnconfig) is used, any `custom_name ` +properties used in [ColumnConfig](#columnconfig) will take precedence and any overlapped values in `custom_column_names` +will be discarded. ::: -A [JSONObject](https://tools.ietf.org/html/rfc7159) of Postgres column -name to GraphQL name mapping +A [JSONObject](https://tools.ietf.org/html/rfc7159) of Postgres column name to GraphQL name mapping ``` { @@ -1203,8 +1189,7 @@ name to GraphQL name mapping ## WebhookURL {#webhookurl} -A String value which supports templating environment variables enclosed -in `{{` and `}}`. +A String value which supports templating environment variables enclosed in `{{` and `}}`.
@@ -1223,8 +1208,7 @@ Template example: `https://{{ACTION_API_DOMAIN}}/create-user` | name | true | String | Name of the header | | value | true | String | Value of the header | -The `value` field supports templating environment variables enclosed in -`{{` and `}}`. +The `value` field supports templating environment variables enclosed in `{{` and `}}`. Template example: `header-{{HEADER_FROM_ENV}}` @@ -1237,9 +1221,7 @@ Template example: `header-{{HEADER_FROM_ENV}}` ## GraphQLType {#graphqltype} -A GraphQL [Type -Reference](https://spec.graphql.org/June2018/#sec-Type-References) -string. +A GraphQL [Type Reference](https://spec.graphql.org/June2018/#sec-Type-References) string.
@@ -1249,13 +1231,11 @@ string.
-Example: `String!` for non-nullable String type and `[String]` for array -of String types +Example: `String!` for non-nullable String type and `[String]` for array of String types ## GraphQLName {#graphqlname} -A string literal that conform to [GraphQL -spec](https://spec.graphql.org/June2018/#Name). +A string literal that conform to [GraphQL spec](https://spec.graphql.org/June2018/#Name).
@@ -1289,8 +1269,8 @@ spec](https://spec.graphql.org/June2018/#Name). :::info Note -The `GraphQL Types` used in creating an action must be defined before -via [Custom Types](/api-reference/metadata-api/custom-types.mdx) +The `GraphQL Types` used in creating an action must be defined before via +[Custom Types](/api-reference/metadata-api/custom-types.mdx) ::: @@ -1312,11 +1292,11 @@ via [Custom Types](/api-reference/metadata-api/custom-types.mdx) ## LogicalModelField {#logicalmodelfield} -| Key | Required | Schema | Description | -| ----------- | -------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------| -| name | true | `String` | The name of the Logical Model field | -| type | true | [Logical Model Type](#logicalmodeltype) | A Logical Model field type | -| description | false | `String` | An extended description of the field | +| Key | Required | Schema | Description | +| ----------- | -------- | --------------------------------------- | ------------------------------------ | +| name | true | `String` | The name of the Logical Model field | +| type | true | [Logical Model Type](#logicalmodeltype) | A Logical Model field type | +| description | false | `String` | An extended description of the field | ## LogicalModelType {#logicalmodeltype} @@ -1324,24 +1304,24 @@ A Logical Model type is one of either: A scalar: -| Key | Required | Schema | Description | -| ----------- | -------- | --------- | ------------------------------------------------------------------------------------------------| -| scalar | true | `String` | The type of the exposed column, according to the underlying data source | -| nullable | false | `Boolean` | True if the field should be exposed over the GraphQL API as a nullable field (default: `false`) | +| Key | Required | Schema | Description | +| -------- | -------- | --------- | ----------------------------------------------------------------------------------------------- | +| scalar | true | `String` | The type of the exposed column, according to the underlying data source | +| nullable | false | `Boolean` | True if the field should be exposed over the GraphQL API as a nullable field (default: `false`) | An array: -| Key | Required | Schema | Description | -| ----------- | -------- | --------- | ------------------------------------------------------------------------------------------------| -| array | true | [Logical Model Type](#logicalmodeltype) | A Logical Model type, which this denotes an array of | -| nullable | false | `Boolean` | True if the field should be exposed over the GraphQL API as a nullable field (default: `false`) | +| Key | Required | Schema | Description | +| -------- | -------- | --------------------------------------- | ----------------------------------------------------------------------------------------------- | +| array | true | [Logical Model Type](#logicalmodeltype) | A Logical Model type, which this denotes an array of | +| nullable | false | `Boolean` | True if the field should be exposed over the GraphQL API as a nullable field (default: `false`) | A reference to another logical model: -| Key | Required | Schema | Description | -| ----------- | -------- | --------- | -------------------------------------------------------------------------------------------------------| -| logical_model | true | [Logical Model Type](#logicalmodeltype) | A Logical Model type, which this refers to. Recursive and mutually recursive references are permitted. | -| nullable | false | `Boolean` | True if the field should be exposed over the GraphQL API as a nullable field (default: `false`) | +| Key | Required | Schema | Description | +| ------------- | -------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| logical_model | true | [Logical Model Type](#logicalmodeltype) | A Logical Model type, which this refers to. Recursive and mutually recursive references are permitted. | +| nullable | false | `Boolean` | True if the field should be exposed over the GraphQL API as a nullable field (default: `false`) | ## NativeQueryArgument {#nativequeryargument} @@ -1354,7 +1334,7 @@ A reference to another logical model: ## NativeQueryRelationship {#nativequeryrelationship} | Key | Required | Schema | Description | -| --------------- | -------- | ------------------------------------- | ---------------------------------------------------------------- | +| ------------------- | -------- | ------------------------------------- | ---------------------------------------------------------------- | | remote_native_query | true | `String` | The Native Query to which the relationship has to be established | | column_mapping | true | Object (local-column : remote-column) | Mapping of columns from current table to remote table | @@ -1386,23 +1366,19 @@ A reference to another logical model: :::info Note -Currently, only functions which satisfy the following constraints can be -exposed over the GraphQL API (_terminology from_ [Postgres -docs](https://www.postgresql.org/docs/current/sql-createfunction.html)): +Currently, only functions which satisfy the following constraints can be exposed over the GraphQL API (_terminology +from_ [Postgres docs](https://www.postgresql.org/docs/current/sql-createfunction.html)): -- **Function behavior**: `STABLE` or `IMMUTABLE` functions may _only_ - be exposed as queries (i.e. with `exposed_as: query`) `VOLATILE` - functions may be exposed as mutations or queries. -- **Return type**: MUST be `SETOF ` OR `` - where `` is already tracked +- **Function behavior**: `STABLE` or `IMMUTABLE` functions may _only_ be exposed as queries (i.e. with + `exposed_as: query`) `VOLATILE` functions may be exposed as mutations or queries. +- **Return type**: MUST be `SETOF ` OR `` where `` is already tracked - **Argument modes**: ONLY `IN` ::: ## InputObjectType {#inputobjecttype} -A simple JSON object to define [GraphQL Input -Object](https://spec.graphql.org/June2018/#sec-Input-Objects) +A simple JSON object to define [GraphQL Input Object](https://spec.graphql.org/June2018/#sec-Input-Objects) | Key | Required | Schema | Description | | ----------- | -------- | ---------------------------------------------- | ------------------------------------ | @@ -1420,8 +1396,7 @@ Object](https://spec.graphql.org/June2018/#sec-Input-Objects) ## ObjectType {#objecttype} -A simple JSON object to define [GraphQL -Object](https://spec.graphql.org/June2018/#sec-Objects) +A simple JSON object to define [GraphQL Object](https://spec.graphql.org/June2018/#sec-Objects) | Key | Required | Schema | Description | | ------------- | -------- | -------------------------------------------------- | ------------------------------------------ | @@ -1449,8 +1424,7 @@ Object](https://spec.graphql.org/June2018/#sec-Objects) ## ScalarType {#scalartype} -A simple JSON object to define [GraphQL -Scalar](https://spec.graphql.org/June2018/#sec-Scalars) +A simple JSON object to define [GraphQL Scalar](https://spec.graphql.org/June2018/#sec-Scalars) | Key | Required | Schema | Description | | ----------- | -------- | --------------------------- | ------------------------------ | @@ -1459,8 +1433,7 @@ Scalar](https://spec.graphql.org/June2018/#sec-Scalars) ## EnumType {#enumtype} -A simple JSON object to define [GraphQL -Enum](https://spec.graphql.org/June2018/#sec-Enums) +A simple JSON object to define [GraphQL Enum](https://spec.graphql.org/June2018/#sec-Enums) | Key | Required | Schema | Description | | ----------- | -------- | -------------------------------- | ---------------------------- | @@ -1522,7 +1495,8 @@ Enum](https://spec.graphql.org/June2018/#sec-Enums) :::tip Supported from -Version 2 is supported in `v2.5.0` and above. You must remove any "version 2" schemas from your Metadata prior to downgrading to `v2.4.0` or earlier +Version 2 is supported in `v2.5.0` and above. You must remove any "version 2" schemas from your Metadata prior to +downgrading to `v2.4.0` or earlier ::: @@ -1548,7 +1522,8 @@ HGE provides the following functions that can be used in the template: "%3Ffoo%3Dbar%2Fbaz" ``` -- `getSessionVariable`: This function takes a string and returns the session variable of the given name. This function can throw the following errors: +- `getSessionVariable`: This function takes a string and returns the session variable of the given name. This function + can throw the following errors: - Session variable {variable name} not found - Session variable name should be a string @@ -1671,9 +1646,8 @@ Note: _One_ of and _only one_ of `to_source` and `to_remote_schema` must be pres } ``` -`RemoteField` is a recursive tree structure that points to the field in -the Remote Schema that needs to be joined with. It is recursive because -the remote field maybe nested deeply in the Remote Schema. +`RemoteField` is a recursive tree structure that points to the field in the Remote Schema that needs to be joined with. +It is recursive because the remote field maybe nested deeply in the Remote Schema. Examples: @@ -1759,6 +1733,7 @@ Table columns can be referred by prefixing `$` e.g `$id`. | sources | true | `'*'` \| [[SourceName]](#sourcename) | Sources for which to update the cleaner status (or all sources when `'*'` is provided) | ## EventTriggerQualifier {#eventtriggerqualifier} + | Key | required | Schema | Description | | -------------- | -------- | ----------------------------- | ----------------------------------------- | | event_triggers | true | [[TriggerName]](#triggername) | List of trigger names | @@ -1786,50 +1761,54 @@ Table columns can be referred by prefixing `$` e.g `$id`. | max_reqs_per_min | true | Integer | Maximum requests per minute to be allowed | ## PGConnectionTemplate {#pgconnectiontemplate} + | Key | required | Schema | Description | | -------- | -------- | ------ | --------------------------------------------------------- | | template | true | String | Template for the dynamic DB connection | | version | false | Int | Version of the template (Possible value is 1, default: 1) | - ## ConnectionSetElementConfig {#connectionsetelementconfig} -| Key | required | Schema | Description | -| ----------------- | -------- | ------------------------------------------------- | ------------------------------------ | -| name | true | String | name of the connection | -| connection_info | true | [PGSourceConnectionInfo](#pgsourceconnectioninfo) | Connection parameters for the source | +| Key | required | Schema | Description | +| --------------- | -------- | ------------------------------------------------- | ------------------------------------ | +| name | true | String | name of the connection | +| connection_info | true | [PGSourceConnectionInfo](#pgsourceconnectioninfo) | Connection parameters for the source | ## RequestContext {#requestcontext} | Key | required | Schema | Description | -|---------|----------|----------------------------------------------------------------|---------------------------| +| ------- | -------- | -------------------------------------------------------------- | ------------------------- | | headers | false | Object ([HeaderKey](#headerkey) : [HeaderValue](#headervalue)) | Request header | | session | false | Object (String : String) | Request session variables | | query | false | [QueryContext](#queryContext) | Operation details | ## QueryContext {#queryContext} -| Key | required | Schema | Description | -|----------------|----------|---------------------------------|-------------------------------| -| operation_type | true | query | mutation | subscription | Type of the graphql operation | -| operation_name | false | String | Name of the graphql operation | +| Key | required | Schema | Description | +| -------------- | -------- | ------ | ----------------------------- | ------------ | ----------------------------- | +| operation_type | true | query | mutation | subscription | Type of the graphql operation | +| operation_name | false | String | Name of the graphql operation | ## Attribute {#attribute} + | Key | required | Schema | Description | -|-------|----------|--------|------------------------| +| ----- | -------- | ------ | ---------------------- | | name | true | String | Name of the attribute | | value | true | String | Value of the attribute | ## OTLPExporter {#otlpexporter} -| Key | required | Schema | Description | -|-----------------------|----------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| -| otlp_traces_endpoint | true | `String` | OpenTelemetry compliant receiver endpoint URL for traces (usually having path "/v1/traces") | -| otlp_metrics_endpoint | true | `String` | OpenTelemetry compliant receiver endpoint URL for metrics (usually having path "/v1/metrics") | -| protocol | false | `String` | Protocol to be used for the communication with the receiver. Currently only supports `http/protobuf`| -| headers | false | \[[HeaderFromValue](#headerfromvalue) \| [HeaderFromEnv](#headerfromenv) \] | List of defined headers to be sent to the receiver | -| resource_attributes | false | \[[Attribute](#attribute)\] | List of resource attributes to be sent to the receiver | + +| Key | required | Schema | Description | +| --------------------- | -------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| otlp_traces_endpoint | true | `String` | OpenTelemetry compliant receiver endpoint URL for traces (usually having path "/v1/traces") | +| otlp_metrics_endpoint | true | `String` | OpenTelemetry compliant receiver endpoint URL for metrics (usually having path "/v1/metrics") | +| protocol | false | `String` | Protocol to be used for the communication with the receiver. Currently only supports `http/protobuf` | +| headers | false | \[[HeaderFromValue](#headerfromvalue) \| [HeaderFromEnv](#headerfromenv) \] | List of defined headers to be sent to the receiver | +| resource_attributes | false | \[[Attribute](#attribute)\] | List of resource attributes to be sent to the receiver | +| traces_propagators | false | \["tracecontext"\] | List of trace propagations to exchange context between services and processes | ## OpenTelemetryBatchSpanProcessor {#opentelemetrybatchspanprocessor} -| Key | required | Schema | Description | -|-----------------------|----------|----------------------------------|---------------------------------------------------------------------------| -| max_export_batch_size | false | `Integer` | Maximum number of spans allowed per export request. Default value is 512 | + +| Key | required | Schema | Description | +| --------------------- | -------- | --------- | ------------------------------------------------------------------------ | +| max_export_batch_size | false | `Integer` | Maximum number of spans allowed per export request. Default value is 512 | diff --git a/docs/docs/observability/opentelemetry.mdx b/docs/docs/observability/opentelemetry.mdx index b1cd844e5bd..ad2c72a8f85 100644 --- a/docs/docs/observability/opentelemetry.mdx +++ b/docs/docs/observability/opentelemetry.mdx @@ -157,6 +157,29 @@ currently supported. Batch size is the maximum number of data points (spans in the context of traces) allowed per export request made to the observability tool. Default size is 512. +### Trace Propagations + +Trace Propagation implements the mechanism that exchanges context between services and processes. It serializes or +deserializes the context object and provides the relevant trace information to be propagated from one service to +another. GraphQL Engine supports the following propagation mechanisms: + +- [B3 Propagation](https://github.com/openzipkin/b3-propagation) +- [W3C Trace Context](https://www.w3.org/TR/trace-context) + +:::info Trace Propagation support + +W3C Trace Context is supported for Hasura GraphQL Engine versions `v2.35.0` and above. + +::: + +B3 propagation is enabled by default. You can enable other protocols in the `OpenTelemetry Exporter` configuration. + + + ### Headers Headers are _(optionally)_ added to every request made by Hasura to the observability tool. They are generally diff --git a/docs/static/img/enterprise/open-telemetry-trace-propagation.png b/docs/static/img/enterprise/open-telemetry-trace-propagation.png new file mode 100644 index 0000000000000000000000000000000000000000..085ef4c90bc7acf28d8a80c742e0aa09e0fef5d6 GIT binary patch literal 97089 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 literal 0 HcmV?d00001 diff --git a/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetry/OpenTelemetry.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetry/OpenTelemetry.stories.tsx index 200c40968e8..137fbc72a1d 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetry/OpenTelemetry.stories.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetry/OpenTelemetry.stories.tsx @@ -58,6 +58,7 @@ export const DisabledWithoutLicense: StoryObj = { attributes: [], dataType: ['traces'], connectionType: 'http/protobuf', + tracesPropagators: [], }, withoutLicense: true, @@ -91,6 +92,7 @@ export const Enabled: StoryObj = { attributes: [], dataType: ['traces'], connectionType: 'http/protobuf', + tracesPropagators: [], }, }, }; @@ -156,6 +158,7 @@ const metadataLoadedProps: ComponentPropsWithoutRef = { connectionType: 'http/protobuf', tracesEndpoint: 'http://localhost:1234', metricsEndpoint: 'http://localhost:1234', + tracesPropagators: ['tracecontext'], headers: [{ name: 'foo', value: 'bar', type: 'from_value' }], attributes: [{ name: 'foo', value: 'bar', type: 'from_value' }], }, diff --git a/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetry/components/Form/Form.tsx b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetry/components/Form/Form.tsx index 2eb27de3dd0..60ca8e25e7e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetry/components/Form/Form.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetry/components/Form/Form.tsx @@ -2,11 +2,15 @@ import * as React from 'react'; import clsx from 'clsx'; import { Button } from '../../../../../new-components/Button'; -import { useConsoleForm, InputField } from '../../../../../new-components/Form'; +import { + useConsoleForm, + InputField, + CheckboxesField, +} from '../../../../../new-components/Form'; import { RequestHeadersSelector } from '../../../../../new-components/RequestHeadersSelector'; import type { FormValues } from './schema'; -import { formSchema } from './schema'; +import { formSchema, tracesPropagatorSchema } from './schema'; import { Toggle } from './components/Toggle'; import { useResetDefaultFormValues } from './hooks/useResetDefaultFormValues'; import { CollapsibleFieldWrapper } from './components/CollapsibleFieldWrapper'; @@ -119,6 +123,21 @@ export function Form(props: FormProps) { clearButton loading={skeletonMode} /> +

+ ({ + label: option, + value: option, + disabled: option === 'b3', + }))} + /> +
maybe headers (\(AgentLicenseKey key) -> ("X-Hasura-License", key) : headers) _accAgentLicenseKey - (tracedReq, responseOrException) <- traceHTTPRequest transformableReq' \tracedReq -> + (tracedReq, responseOrException) <- traceHTTPRequest b3TraceContextPropagator transformableReq' \tracedReq -> fmap (tracedReq,) . liftIO . try @HTTP.HttpException $ HTTP.httpLbs tracedReq _accHttpManager logAgentRequest _accLogger tracedReq responseOrException case responseOrException of diff --git a/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs b/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs index e5a8f95b457..96002199b0e 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs @@ -77,6 +77,7 @@ import Hasura.RQL.Types.Permission import Hasura.RQL.Types.Schema.Options qualified as Options import Hasura.Server.Utils import Hasura.Session +import Hasura.Tracing (b3TraceContextPropagator) import Hasura.Tracing qualified as Tracing import Language.GraphQL.Draft.Syntax qualified as G import Network.HTTP.Client.Transformable qualified as HTTP @@ -512,7 +513,7 @@ validateMutation env manager logger userInfo (ResolvedWebhook urlText) confHeade & Lens.set HTTP.body (HTTP.RequestBodyLBS $ J.encode requestBody) & Lens.set HTTP.timeout (HTTP.responseTimeoutMicro (unTimeout timeout * 1000000)) -- (default: 10 seconds) httpResponse <- - Tracing.traceHTTPRequest request $ \request' -> + Tracing.traceHTTPRequest b3TraceContextPropagator request $ \request' -> liftIO . try $ HTTP.httpLbs request' manager case httpResponse of diff --git a/server/src-lib/Hasura/Eventing/EventTrigger.hs b/server/src-lib/Hasura/Eventing/EventTrigger.hs index 20f3b3c09ea..282c5fd10f6 100644 --- a/server/src-lib/Hasura/Eventing/EventTrigger.hs +++ b/server/src-lib/Hasura/Eventing/EventTrigger.hs @@ -76,6 +76,7 @@ import Hasura.RQL.Types.Backend import Hasura.RQL.Types.BackendType import Hasura.RQL.Types.Common import Hasura.RQL.Types.EventTrigger +import Hasura.RQL.Types.OpenTelemetry (getOtelTracesPropagator) import Hasura.RQL.Types.SchemaCache import Hasura.RQL.Types.Source import Hasura.SQL.AnyBackend qualified as AB @@ -462,7 +463,7 @@ processEventQueue logger statsLogger httpMgr getSchemaCache getEventEngineCtx ac Tracing.samplingStateFromHeader $ e ^? JL.key "trace_context" . JL.key "sampling_state" . JL._String - pure $ Tracing.TraceContext traceId freshSpanId parentSpanId samplingState + pure $ Tracing.TraceContext traceId freshSpanId parentSpanId samplingState Tracing.emptyTraceState processEvent :: forall io r b. @@ -542,6 +543,7 @@ processEventQueue logger statsLogger httpMgr getSchemaCache getEventEngineCtx ac $ mkRequest headers httpTimeout payload requestTransform (_envVarValue webhook) >>= \reqDetails -> do let request = extractRequest reqDetails + tracesPropagator = getOtelTracesPropagator $ scOpenTelemetryConfig cache logger' res details = do logHTTPForET res extraLogCtx details (_envVarName webhook) logHeaders triggersErrorLogLevelStatus liftIO $ do @@ -571,7 +573,7 @@ processEventQueue logger statsLogger httpMgr getSchemaCache getEventEngineCtx ac liftIO $ EKG.Gauge.dec $ smNumEventHTTPWorkers serverMetrics liftIO $ Prometheus.Gauge.dec (eventTriggerHTTPWorkers eventTriggerMetrics) ) - (invokeRequest reqDetails responseTransform (_rdSessionVars reqDetails) logger') + (invokeRequest reqDetails responseTransform (_rdSessionVars reqDetails) logger' tracesPropagator) pure (request, resp) case eitherReqRes of Right (req, resp) -> do diff --git a/server/src-lib/Hasura/Eventing/HTTP.hs b/server/src-lib/Hasura/Eventing/HTTP.hs index a0b81e973e9..41e93982cde 100644 --- a/server/src-lib/Hasura/Eventing/HTTP.hs +++ b/server/src-lib/Hasura/Eventing/HTTP.hs @@ -363,13 +363,14 @@ invokeRequest :: Maybe Transform.ResponseTransform -> Maybe SessionVariables -> ((Either (HTTPErr a) (HTTPResp a)) -> RequestDetails -> m ()) -> + HttpPropagator -> m (HTTPResp a) -invokeRequest reqDetails@RequestDetails {..} respTransform' sessionVars logger = do +invokeRequest reqDetails@RequestDetails {..} respTransform' sessionVars logger tracesPropagator = do let finalReq = fromMaybe _rdOriginalRequest _rdTransformedRequest reqBody = fromMaybe J.Null $ preview (HTTP.body . HTTP._RequestBodyLBS) finalReq >>= J.decode @J.Value manager <- asks getter -- Perform the HTTP Request - eitherResp <- traceHTTPRequest finalReq $ runHTTP manager + eitherResp <- traceHTTPRequest tracesPropagator finalReq $ runHTTP manager -- Log the result along with the pre/post transformation Request data logger eitherResp reqDetails resp <- eitherResp `onLeft` (throwError . HTTPError reqBody) diff --git a/server/src-lib/Hasura/Eventing/ScheduledTrigger.hs b/server/src-lib/Hasura/Eventing/ScheduledTrigger.hs index 3679a0fedc7..28afaed6488 100644 --- a/server/src-lib/Hasura/Eventing/ScheduledTrigger.hs +++ b/server/src-lib/Hasura/Eventing/ScheduledTrigger.hs @@ -156,6 +156,7 @@ import Hasura.RQL.DDL.Webhook.Transform import Hasura.RQL.Types.Common import Hasura.RQL.Types.EventTrigger import Hasura.RQL.Types.Eventing +import Hasura.RQL.Types.OpenTelemetry (getOtelTracesPropagator) import Hasura.RQL.Types.ScheduledTrigger import Hasura.RQL.Types.SchemaCache import Hasura.SQL.Types @@ -247,13 +248,14 @@ processCronEvents :: ) => L.Logger L.Hasura -> HTTP.Manager -> + SchemaCache -> ScheduledTriggerMetrics -> [CronEvent] -> HashMap TriggerName CronTriggerInfo -> TVar (Set.Set CronEventId) -> TriggersErrorLogLevelStatus -> m () -processCronEvents logger httpMgr scheduledTriggerMetrics cronEvents cronTriggersInfo lockedCronEvents triggersErrorLogLevelStatus = do +processCronEvents logger httpMgr sc scheduledTriggerMetrics cronEvents cronTriggersInfo lockedCronEvents triggersErrorLogLevelStatus = do -- save the locked cron events that have been fetched from the -- database, the events stored here will be unlocked in case a -- graceful shutdown is initiated in midst of processing these events @@ -284,6 +286,7 @@ processCronEvents logger httpMgr scheduledTriggerMetrics cronEvents cronTriggers runExceptT $ flip runReaderT (logger, httpMgr) $ processScheduledEvent + sc scheduledTriggerMetrics id' ctiHeaders @@ -319,6 +322,7 @@ processOneOffScheduledEvents :: Env.Environment -> L.Logger L.Hasura -> HTTP.Manager -> + SchemaCache -> ScheduledTriggerMetrics -> [OneOffScheduledEvent] -> TVar (Set.Set OneOffScheduledEventId) -> @@ -328,6 +332,7 @@ processOneOffScheduledEvents env logger httpMgr + schemaCache scheduledTriggerMetrics oneOffEvents lockedOneOffScheduledEvents @@ -363,7 +368,7 @@ processOneOffScheduledEvents Right (webhookEnvRecord, eventHeaderInfo) -> do let processScheduledEventAction = flip runReaderT (logger, httpMgr) - $ processScheduledEvent scheduledTriggerMetrics _ooseId eventHeaderInfo retryCtx payload webhookEnvRecord OneOff triggersErrorLogLevelStatus + $ processScheduledEvent schemaCache scheduledTriggerMetrics _ooseId eventHeaderInfo retryCtx payload webhookEnvRecord OneOff triggersErrorLogLevelStatus eventTimeout = unrefine $ strcTimeoutSeconds $ _ooseRetryConf @@ -419,14 +424,15 @@ processScheduledTriggers getEnvHook logger statsLogger httpMgr scheduledTriggerM return $ Forever () $ const do - cronTriggersInfo <- scCronTriggers <$> liftIO getSC + sc <- liftIO getSC env <- liftIO getEnvHook + let cronTriggersInfo = scCronTriggers sc getScheduledEventsForDelivery (HashMap.keys cronTriggersInfo) >>= \case Left e -> logInternalError e Right (cronEvents, oneOffEvents) -> do logFetchedScheduledEventsStats statsLogger (CronEventsCount $ length cronEvents) (OneOffScheduledEventsCount $ length oneOffEvents) - processCronEvents logger httpMgr scheduledTriggerMetrics cronEvents cronTriggersInfo leCronEvents triggersErrorLogLevelStatus - processOneOffScheduledEvents env logger httpMgr scheduledTriggerMetrics oneOffEvents leOneOffEvents triggersErrorLogLevelStatus + processCronEvents logger httpMgr sc scheduledTriggerMetrics cronEvents cronTriggersInfo leCronEvents triggersErrorLogLevelStatus + processOneOffScheduledEvents env logger httpMgr sc scheduledTriggerMetrics oneOffEvents leOneOffEvents triggersErrorLogLevelStatus -- NOTE: cron events are scheduled at times with minute resolution (as on -- unix), while one-off events can be set for arbitrary times. The sleep -- time here determines how overdue a scheduled event (cron or one-off) @@ -444,6 +450,7 @@ processScheduledEvent :: MonadMetadataStorage m, MonadError QErr m ) => + SchemaCache -> ScheduledTriggerMetrics -> ScheduledEventId -> [EventHeaderInfo] -> @@ -453,7 +460,7 @@ processScheduledEvent :: ScheduledEventType -> TriggersErrorLogLevelStatus -> m () -processScheduledEvent scheduledTriggerMetrics eventId eventHeaders retryCtx payload webhookUrl type' triggersErrorLogLevelStatus = +processScheduledEvent schemaCache scheduledTriggerMetrics eventId eventHeaders retryCtx payload webhookUrl type' triggersErrorLogLevelStatus = Tracing.newTrace Tracing.sampleAlways traceNote do currentTime <- liftIO getCurrentTime let retryConf = _rctxConf retryCtx @@ -476,6 +483,7 @@ processScheduledEvent scheduledTriggerMetrics eventId eventHeaders retryCtx payl $ mkRequest headers httpTimeout webhookReqBody requestTransform (_envVarValue webhookUrl) >>= \reqDetails -> do let request = extractRequest reqDetails + tracesPropagator = getOtelTracesPropagator $ scOpenTelemetryConfig schemaCache logger e d = do logHTTPForST e extraLogCtx d (_envVarName webhookUrl) decodedHeaders triggersErrorLogLevelStatus liftIO $ do @@ -495,7 +503,7 @@ processScheduledEvent scheduledTriggerMetrics eventId eventHeaders retryCtx payl (OneOff, Left _err) -> Prometheus.Counter.inc (stmOneOffEventsInvocationTotalFailure scheduledTriggerMetrics) (OneOff, Right _) -> Prometheus.Counter.inc (stmOneOffEventsInvocationTotalSuccess scheduledTriggerMetrics) sessionVars = _rdSessionVars reqDetails - resp <- invokeRequest reqDetails responseTransform sessionVars logger + resp <- invokeRequest reqDetails responseTransform sessionVars logger tracesPropagator pure (request, resp) case eitherReqRes of Right (req, resp) -> diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index 39d30c72a6f..e415f58ee42 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -55,6 +55,7 @@ import Hasura.RQL.Types.Allowlist import Hasura.RQL.Types.Backend import Hasura.RQL.Types.BackendType import Hasura.RQL.Types.Common +import Hasura.RQL.Types.OpenTelemetry (getOtelTracesPropagator) import Hasura.RQL.Types.Roles (adminRoleName) import Hasura.RQL.Types.SchemaCache import Hasura.RQL.Types.Subscription @@ -364,6 +365,7 @@ getResolvedExecPlan maybeOperationName reqId = do let gCtx = makeGQLContext userInfo sc queryType + tracesPropagator = getOtelTracesPropagator $ scOpenTelemetryConfig sc -- Construct the full 'ResolvedExecutionPlan' from the 'queryParts :: SingleOperation'. (parameterizedQueryHash, resolvedExecPlan) <- @@ -373,6 +375,7 @@ getResolvedExecPlan EQ.convertQuerySelSet env logger + tracesPropagator prometheusMetrics gCtx userInfo @@ -393,6 +396,7 @@ getResolvedExecPlan EM.convertMutationSelectionSet env logger + tracesPropagator prometheusMetrics gCtx sqlGenCtx diff --git a/server/src-lib/Hasura/GraphQL/Execute/Action.hs b/server/src-lib/Hasura/GraphQL/Execute/Action.hs index 80568996d2a..7b88a5477d3 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/Action.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/Action.hs @@ -74,6 +74,7 @@ import Hasura.RQL.Types.ComputedField import Hasura.RQL.Types.CustomTypes import Hasura.RQL.Types.Eventing import Hasura.RQL.Types.Headers (HeaderConf) +import Hasura.RQL.Types.OpenTelemetry (getOtelTracesPropagator) import Hasura.RQL.Types.Roles (adminRoleName) import Hasura.RQL.Types.Schema.Options qualified as Options import Hasura.RQL.Types.SchemaCache @@ -147,12 +148,13 @@ resolveActionExecution :: HTTP.Manager -> Env.Environment -> L.Logger L.Hasura -> + Tracing.HttpPropagator -> PrometheusMetrics -> IR.AnnActionExecution Void -> ActionExecContext -> Maybe GQLQueryText -> ActionExecution -resolveActionExecution httpManager env logger prometheusMetrics IR.AnnActionExecution {..} ActionExecContext {..} gqlQueryText = +resolveActionExecution httpManager env logger tracesPropagator prometheusMetrics IR.AnnActionExecution {..} ActionExecContext {..} gqlQueryText = ActionExecution $ first (encJFromOrderedValue . makeActionResponseNoRelations _aaeFields _aaeOutputType _aaeOutputFields True) <$> runWebhook where handlerPayload = ActionWebhookPayload (ActionContext _aaeName) _aecSessionVariables _aaePayload gqlQueryText @@ -166,6 +168,7 @@ resolveActionExecution httpManager env logger prometheusMetrics IR.AnnActionExec $ callWebhook env httpManager + tracesPropagator prometheusMetrics _aaeOutputType _aaeOutputFields @@ -471,8 +474,10 @@ asyncActionsProcessor getEnvHook logger getSCFromRef' getFetchInterval lockedAct -- we check for async actions to process. Skip -> liftIO $ sleep $ seconds 1 Interval sleepTime -> do - actionCache <- scActions <$> liftIO getSCFromRef' - let asyncActions = + schemaCache <- liftIO getSCFromRef' + let actionCache = scActions schemaCache + tracesPropagator = getOtelTracesPropagator $ scOpenTelemetryConfig schemaCache + asyncActions = HashMap.filter ((== ActionMutation ActionAsynchronous) . (^. aiDefinition . adType)) actionCache unless (HashMap.null asyncActions) $ do -- fetch undelivered action events only when there's at least @@ -488,11 +493,11 @@ asyncActionsProcessor getEnvHook logger getSCFromRef' getFetchInterval lockedAct -- locked action events set TVar is empty, it will mean that there are -- no events that are in the 'processing' state saveLockedEvents (map (EventId . actionIdToText . _aliId) asyncInvocations) lockedActionEvents - LA.mapConcurrently_ (callHandler actionCache) asyncInvocations + LA.mapConcurrently_ (callHandler actionCache tracesPropagator) asyncInvocations liftIO $ sleep $ milliseconds (unrefine sleepTime) where - callHandler :: ActionCache -> ActionLogItem -> m () - callHandler actionCache actionLogItem = + callHandler :: ActionCache -> Tracing.HttpPropagator -> ActionLogItem -> m () + callHandler actionCache tracesPropagator actionLogItem = Tracing.newTrace Tracing.sampleAlways "async actions processor" do let ActionLogItem actionId @@ -521,6 +526,7 @@ asyncActionsProcessor getEnvHook logger getSCFromRef' getFetchInterval lockedAct $ callWebhook env appEnvManager + tracesPropagator appEnvPrometheusMetrics outputType outputFields @@ -549,6 +555,7 @@ callWebhook :: ) => Env.Environment -> HTTP.Manager -> + Tracing.HttpPropagator -> PrometheusMetrics -> GraphQLType -> IR.ActionOutputFields -> @@ -564,6 +571,7 @@ callWebhook :: callWebhook env manager + tracesPropagator prometheusMetrics outputType outputFields @@ -617,7 +625,7 @@ callWebhook actualSize = fromMaybe requestBodySize transformedReqSize httpResponse <- - Tracing.traceHTTPRequest actualReq $ \request -> + Tracing.traceHTTPRequest tracesPropagator actualReq $ \request -> liftIO . try $ HTTP.httpLbs request manager let requestInfo = ActionRequestInfo webhookEnvName postPayload (confHeaders <> toHeadersConf clientHeaders) transformedReq diff --git a/server/src-lib/Hasura/GraphQL/Execute/Mutation.hs b/server/src-lib/Hasura/GraphQL/Execute/Mutation.hs index 16ef5ce84db..8764d28388c 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/Mutation.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/Mutation.hs @@ -50,17 +50,18 @@ convertMutationAction :: ) => Env.Environment -> L.Logger L.Hasura -> + Tracing.HttpPropagator -> PrometheusMetrics -> UserInfo -> HTTP.RequestHeaders -> Maybe GH.GQLQueryText -> ActionMutation Void -> m ActionExecutionPlan -convertMutationAction env logger prometheusMetrics userInfo reqHeaders gqlQueryText action = do +convertMutationAction env logger tracesPropagator prometheusMetrics userInfo reqHeaders gqlQueryText action = do httpManager <- askHTTPManager case action of AMSync s -> - pure $ AEPSync $ resolveActionExecution httpManager env logger prometheusMetrics s actionExecContext gqlQueryText + pure $ AEPSync $ resolveActionExecution httpManager env logger tracesPropagator prometheusMetrics s actionExecContext gqlQueryText AMAsync s -> AEPAsyncMutation <$> resolveActionMutationAsync s reqHeaders userSession where @@ -79,6 +80,7 @@ convertMutationSelectionSet :: ) => Env.Environment -> L.Logger L.Hasura -> + Tracing.HttpPropagator -> PrometheusMetrics -> GQLContext -> SQLGenCtx -> @@ -96,6 +98,7 @@ convertMutationSelectionSet :: convertMutationSelectionSet env logger + tracesPropagator prometheusMetrics gqlContext SQLGenCtx {stringifyNum} @@ -150,7 +153,7 @@ convertMutationSelectionSet (actionName, _fch) <- pure $ case noRelsDBAST of AMSync s -> (_aaeName s, _aaeForwardClientHeaders s) AMAsync s -> (_aamaName s, _aamaForwardClientHeaders s) - plan <- convertMutationAction env logger prometheusMetrics userInfo reqHeaders (Just (GH._grQuery gqlUnparsed)) noRelsDBAST + plan <- convertMutationAction env logger tracesPropagator prometheusMetrics userInfo reqHeaders (Just (GH._grQuery gqlUnparsed)) noRelsDBAST pure $ ExecStepAction plan (ActionsInfo actionName _fch) remoteJoins -- `_fch` represents the `forward_client_headers` option from the action -- definition which is currently being ignored for actions that are mutations RFRaw customFieldVal -> flip onLeft throwError =<< executeIntrospection userInfo customFieldVal introspectionDisabledRoles diff --git a/server/src-lib/Hasura/GraphQL/Execute/Query.hs b/server/src-lib/Hasura/GraphQL/Execute/Query.hs index 4a2a0597981..df516926c48 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/Query.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/Query.hs @@ -71,6 +71,7 @@ convertQuerySelSet :: ) => Env.Environment -> L.Logger L.Hasura -> + Tracing.HttpPropagator -> PrometheusMetrics -> GQLContext -> UserInfo -> @@ -87,6 +88,7 @@ convertQuerySelSet :: convertQuerySelSet env logger + tracingPropagator prometheusMetrics gqlContext userInfo @@ -141,6 +143,7 @@ convertQuerySelSet httpManager env logger + tracingPropagator prometheusMetrics s (ActionExecContext reqHeaders (_uiSession userInfo)) diff --git a/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Join.hs b/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Join.hs index 5a1f26578a3..02ddbd1d39a 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Join.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Join.hs @@ -74,8 +74,9 @@ processRemoteJoins :: EncJSON -> Maybe RemoteJoins -> GQLReqUnparsed -> + Tracing.HttpPropagator -> m EncJSON -processRemoteJoins requestId logger agentLicenseKey env requestHeaders userInfo lhs maybeJoinTree gqlreq = +processRemoteJoins requestId logger agentLicenseKey env requestHeaders userInfo lhs maybeJoinTree gqlreq tracesPropagator = Tracing.newSpan "Process remote joins" $ forRemoteJoins maybeJoinTree lhs \joinTree -> do lhsParsed <- JO.eitherDecode (encJToLBS lhs) @@ -123,7 +124,7 @@ processRemoteJoins requestId logger agentLicenseKey env requestHeaders userInfo m BL.ByteString callRemoteServer remoteSchemaInfo request = fmap (view _3) - $ execRemoteGQ env userInfo requestHeaders remoteSchemaInfo request + $ execRemoteGQ env tracesPropagator userInfo requestHeaders remoteSchemaInfo request -- | Fold the join tree. -- diff --git a/server/src-lib/Hasura/GraphQL/RemoteServer.hs b/server/src-lib/Hasura/GraphQL/RemoteServer.hs index 74528be5cad..9ad50ada3fd 100644 --- a/server/src-lib/Hasura/GraphQL/RemoteServer.hs +++ b/server/src-lib/Hasura/GraphQL/RemoteServer.hs @@ -63,7 +63,7 @@ fetchRemoteSchema :: m (IntrospectionResult, BL.ByteString, RemoteSchemaInfo) fetchRemoteSchema env schemaSampledFeatureFlags rsDef = do (_, _, rawIntrospectionResult) <- - execRemoteGQ env adminUserInfo [] rsDef introspectionQuery + execRemoteGQ env Tracing.b3TraceContextPropagator adminUserInfo [] rsDef introspectionQuery (ir, rsi) <- stitchRemoteSchema schemaSampledFeatureFlags rawIntrospectionResult rsDef -- The 'rawIntrospectionResult' contains the 'Bytestring' response of -- the introspection result of the remote server. We store this in the @@ -135,6 +135,7 @@ execRemoteGQ :: ProvidesNetwork m ) => Env.Environment -> + Tracing.HttpPropagator -> UserInfo -> [HTTP.Header] -> ValidatedRemoteSchemaDef -> @@ -142,7 +143,7 @@ execRemoteGQ :: -- | Returns the response body and headers, along with the time taken for the -- HTTP request to complete m (DiffTime, [HTTP.Header], BL.ByteString) -execRemoteGQ env userInfo reqHdrs rsdef gqlReq@GQLReq {..} = do +execRemoteGQ env tracesPropagator userInfo reqHdrs rsdef gqlReq@GQLReq {..} = do let gqlReqUnparsed = renderGQLReqOutgoing gqlReq when (G._todType _grQuery == G.OperationTypeSubscription) @@ -167,7 +168,7 @@ execRemoteGQ env userInfo reqHdrs rsdef gqlReq@GQLReq {..} = do & set HTTP.timeout (HTTP.responseTimeoutMicro (timeout * 1000000)) manager <- askHTTPManager - Tracing.traceHTTPRequest req \req' -> do + Tracing.traceHTTPRequest tracesPropagator req \req' -> do (time, res) <- withElapsedTime $ liftIO $ try $ HTTP.httpLbs req' manager resp <- onLeft res (throwRemoteSchemaHttp webhookEnvRecord) pure (time, mkSetCookieHeaders resp, resp ^. Wreq.responseBody) diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index 6ec49f93556..c0fa661b9ba 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -73,6 +73,7 @@ import Hasura.RQL.IR import Hasura.RQL.Types.Backend import Hasura.RQL.Types.BackendType import Hasura.RQL.Types.Common +import Hasura.RQL.Types.OpenTelemetry (getOtelTracesPropagator) import Hasura.RQL.Types.ResultCustomization import Hasura.RQL.Types.SchemaCache import Hasura.RemoteSchema.SchemaCache @@ -371,6 +372,8 @@ runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicen forWithKey = flip InsOrdHashMap.traverseWithKey + tracesPropagator = getOtelTracesPropagator $ scOpenTelemetryConfig sc + executePlan :: GQLReqParsed -> (m AnnotatedResponse -> m AnnotatedResponse) -> @@ -470,7 +473,7 @@ runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicen \(EB.DBStepInfo _ sourceConfig genSql tx resolvedConnectionTemplate :: EB.DBStepInfo b) -> runDBQuery @b reqId reqUnparsed fieldName userInfo logger agentLicenseKey sourceConfig (fmap (statsToAnyBackend @b) tx) genSql resolvedConnectionTemplate finalResponse <- - RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed + RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed tracesPropagator pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse [] E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindRemoteSchema @@ -480,7 +483,7 @@ runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicen (time, resp) <- doQErr $ do (time, (resp, _)) <- EA.runActionExecution userInfo aep finalResponse <- - RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed + RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed tracesPropagator pure (time, finalResponse) pure $ AnnotatedResponsePart time Telem.Empty resp [] E.ExecStepRaw json -> do @@ -503,7 +506,7 @@ runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicen \(EB.DBStepInfo _ sourceConfig genSql tx resolvedConnectionTemplate :: EB.DBStepInfo b) -> runDBMutation @b reqId reqUnparsed fieldName userInfo logger agentLicenseKey sourceConfig (fmap EB.arResult tx) genSql resolvedConnectionTemplate finalResponse <- - RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed + RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed tracesPropagator pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse responseHeaders E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindRemoteSchema @@ -513,7 +516,7 @@ runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicen (time, (resp, hdrs)) <- doQErr $ do (time, (resp, hdrs)) <- EA.runActionExecution userInfo aep finalResponse <- - RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed + RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed tracesPropagator pure (time, (finalResponse, hdrs)) pure $ AnnotatedResponsePart time Telem.Empty resp $ fromMaybe [] hdrs E.ExecStepRaw json -> do @@ -526,7 +529,7 @@ runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicen runRemoteGQ fieldName rsi resultCustomizer gqlReq remoteJoins = Tracing.newSpan ("Remote schema query for root field " <>> fieldName) $ do (telemTimeIO_DT, remoteResponseHeaders, resp) <- - doQErr $ E.execRemoteGQ env userInfo reqHeaders (rsDef rsi) gqlReq + doQErr $ E.execRemoteGQ env tracesPropagator userInfo reqHeaders (rsDef rsi) gqlReq value <- extractFieldFromResponse fieldName resultCustomizer resp finalResponse <- doQErr @@ -541,6 +544,7 @@ runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicen (encJFromOrderedValue value) remoteJoins reqUnparsed + tracesPropagator let filteredHeaders = filter ((== "Set-Cookie") . fst) remoteResponseHeaders pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Remote finalResponse filteredHeaders diff --git a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs index 73410aaafec..e82237fe148 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs @@ -78,8 +78,9 @@ import Hasura.Metadata.Class import Hasura.Prelude import Hasura.QueryTags import Hasura.RQL.Types.Common (MetricsConfig (_mcAnalyzeQueryVariables)) +import Hasura.RQL.Types.OpenTelemetry (getOtelTracesPropagator) import Hasura.RQL.Types.ResultCustomization -import Hasura.RQL.Types.SchemaCache (scApiLimits, scMetricsConfig) +import Hasura.RQL.Types.SchemaCache (SchemaCache (scOpenTelemetryConfig), scApiLimits, scMetricsConfig) import Hasura.RemoteSchema.SchemaCache import Hasura.SQL.AnyBackend qualified as AB import Hasura.Server.AppStateRef @@ -488,6 +489,7 @@ onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables let gqlOpType = G._todType queryParts opName = getOpNameFromParsedReq reqParsed maybeOperationName = _unOperationName <$> opName + tracesPropagator = getOtelTracesPropagator $ scOpenTelemetryConfig sc for_ maybeOperationName $ \nm -> -- https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/instrumentation/graphql/ Tracing.attachMetadata [("graphql.operation.name", unName nm)] @@ -549,17 +551,17 @@ onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables genSql resolvedConnectionTemplate finalResponse <- - RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q + RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q tracesPropagator pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse [] E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema - runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins + runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins tracesPropagator E.ExecStepAction actionExecPlan _ remoteJoins -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction (time, (resp, _)) <- doQErr $ do (time, (resp, hdrs)) <- EA.runActionExecution userInfo actionExecPlan finalResponse <- - RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q + RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q tracesPropagator pure (time, (finalResponse, hdrs)) pure $ AnnotatedResponsePart time Telem.Empty resp [] E.ExecStepRaw json -> do @@ -628,19 +630,19 @@ onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables genSql resolvedConnectionTemplate finalResponse <- - RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q + RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q tracesPropagator pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse [] E.ExecStepAction actionExecPlan _ remoteJoins -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction (time, (resp, hdrs)) <- doQErr $ do (time, (resp, hdrs)) <- EA.runActionExecution userInfo actionExecPlan finalResponse <- - RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q + RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q tracesPropagator pure (time, (finalResponse, hdrs)) pure $ AnnotatedResponsePart time Telem.Empty resp $ fromMaybe [] hdrs E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema - runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins + runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins tracesPropagator E.ExecStepRaw json -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindIntrospection buildRaw json @@ -796,12 +798,13 @@ onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables ResultCustomizer -> GQLReqOutgoing -> Maybe RJ.RemoteJoins -> + Tracing.HttpPropagator -> ExceptT (Either GQExecError QErr) (ExceptT () m) AnnotatedResponsePart - runRemoteGQ requestId reqUnparsed fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins = Tracing.newSpan ("Remote schema query for root field " <>> fieldName) $ do + runRemoteGQ requestId reqUnparsed fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins tracesPropagator = Tracing.newSpan ("Remote schema query for root field " <>> fieldName) $ do env <- liftIO $ acEnvironment <$> getAppContext appStateRef (telemTimeIO_DT, _respHdrs, resp) <- doQErr - $ E.execRemoteGQ env userInfo reqHdrs (rsDef rsi) gqlReq + $ E.execRemoteGQ env tracesPropagator userInfo reqHdrs (rsDef rsi) gqlReq value <- hoist lift $ extractFieldFromResponse fieldName resultCustomizer resp finalResponse <- doQErr @@ -816,6 +819,7 @@ onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables (encJFromOrderedValue value) remoteJoins reqUnparsed + tracesPropagator return $ AnnotatedResponsePart telemTimeIO_DT Telem.Remote finalResponse [] WSServerEnv diff --git a/server/src-lib/Hasura/RQL/DDL/OpenTelemetry.hs b/server/src-lib/Hasura/RQL/DDL/OpenTelemetry.hs index 09051adadaf..b9cdf86f322 100644 --- a/server/src-lib/Hasura/RQL/DDL/OpenTelemetry.hs +++ b/server/src-lib/Hasura/RQL/DDL/OpenTelemetry.hs @@ -9,6 +9,7 @@ where import Control.Lens ((.~)) import Data.Bifunctor (first) import Data.Environment (Environment) +import Data.List.Extended (uniques) import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as Text @@ -102,7 +103,10 @@ parseOtelExporterConfig env enabledDataTypes OtelExporterConfig {..} = do Map.fromList $ map (\NameValue {nv_name, nv_value} -> (nv_name, nv_value)) - _oecResourceAttributes + _oecResourceAttributes, + _oteleiTracesPropagator = + mkOtelTracesPropagator + $ uniques (_oecTracesPropagators <> defaultOtelExporterTracesPropagators) } -- Smart constructor. Consistent with defaults. diff --git a/server/src-lib/Hasura/RQL/Types/OpenTelemetry.hs b/server/src-lib/Hasura/RQL/Types/OpenTelemetry.hs index 0f98313670c..69d58cdd10b 100644 --- a/server/src-lib/Hasura/RQL/Types/OpenTelemetry.hs +++ b/server/src-lib/Hasura/RQL/Types/OpenTelemetry.hs @@ -19,6 +19,7 @@ module Hasura.RQL.Types.OpenTelemetry OtelBatchSpanProcessorConfig (..), defaultOtelBatchSpanProcessorConfig, NameValue (..), + TracePropagator (..), -- * Parsed configuration (schema cache) OpenTelemetryInfo (..), @@ -30,6 +31,9 @@ module Hasura.RQL.Types.OpenTelemetry getMaxExportBatchSize, getMaxQueueSize, defaultOtelBatchSpanProcessorInfo, + defaultOtelExporterTracesPropagators, + mkOtelTracesPropagator, + getOtelTracesPropagator, ) where @@ -45,8 +49,10 @@ import Data.Set qualified as Set import GHC.Generics import Hasura.Prelude hiding (first) import Hasura.RQL.Types.Headers (HeaderConf) +import Hasura.Tracing qualified as Tracing import Language.Haskell.TH.Syntax (Lift) import Network.HTTP.Client (Request) +import Network.HTTP.Types (RequestHeaders, ResponseHeaders) -------------------------------------------------------------------------------- @@ -179,7 +185,9 @@ data OtelExporterConfig = OtelExporterConfig _oecHeaders :: [HeaderConf], -- | Attributes to send as the resource attributes of an export request, -- for all telemetry types. - _oecResourceAttributes :: [NameValue] + _oecResourceAttributes :: [NameValue], + -- | Trace propagator to be used to extract and inject trace headers + _oecTracesPropagators :: [TracePropagator] } deriving stock (Eq, Show) @@ -197,12 +205,15 @@ instance HasCodec OtelExporterConfig where AC..= _oecHeaders <*> optionalFieldWithDefault "resource_attributes" defaultOtelExporterResourceAttributes attrsDoc AC..= _oecResourceAttributes + <*> optionalFieldWithDefault "traces_propagators" defaultOtelExporterTracesPropagators propagatorsDocs + AC..= _oecTracesPropagators where tracesEndpointDoc = "Target URL to which the exporter is going to send traces. No default." metricsEndpointDoc = "Target URL to which the exporter is going to send metrics. No default." protocolDoc = "The transport protocol" headersDoc = "Key-value pairs to be used as headers to send with an export request." attrsDoc = "Attributes to send as the resource attributes of an export request. We currently only support string-valued attributes." + propagatorsDocs = "List of propagators to inject and extract traces data from headers." instance FromJSON OtelExporterConfig where parseJSON = J.withObject "OtelExporterConfig" $ \o -> do @@ -216,17 +227,20 @@ instance FromJSON OtelExporterConfig where o .:? "headers" .!= defaultOtelExporterHeaders _oecResourceAttributes <- o .:? "resource_attributes" .!= defaultOtelExporterResourceAttributes + _oecTracesPropagators <- + o .:? "traces_propagators" .!= defaultOtelExporterTracesPropagators pure OtelExporterConfig {..} instance ToJSON OtelExporterConfig where - toJSON (OtelExporterConfig otlpTracesEndpoint otlpMetricsEndpoint protocol headers resourceAttributes) = + toJSON (OtelExporterConfig otlpTracesEndpoint otlpMetricsEndpoint protocol headers resourceAttributes tracesPropagators) = J.object $ catMaybes [ ("otlp_traces_endpoint" .=) <$> otlpTracesEndpoint, ("otlp_metrics_endpoint" .=) <$> otlpMetricsEndpoint, Just $ "protocol" .= protocol, Just $ "headers" .= headers, - Just $ "resource_attributes" .= resourceAttributes + Just $ "resource_attributes" .= resourceAttributes, + Just $ "traces_propagators" .= tracesPropagators ] defaultOtelExporterConfig :: OtelExporterConfig @@ -236,7 +250,8 @@ defaultOtelExporterConfig = _oecMetricsEndpoint = defaultOtelExporterMetricsEndpoint, _oecProtocol = defaultOtelExporterProtocol, _oecHeaders = defaultOtelExporterHeaders, - _oecResourceAttributes = defaultOtelExporterResourceAttributes + _oecResourceAttributes = defaultOtelExporterResourceAttributes, + _oecTracesPropagators = defaultOtelExporterTracesPropagators } -- | Possible protocol to use with OTLP. Currently, only http/protobuf is @@ -294,6 +309,31 @@ instance FromJSON NameValue where nv_value <- o .: "value" pure NameValue {..} +-- Internal helper type for trace propagators +data TracePropagator + = B3 + | TraceContext + deriving stock (Eq, Ord, Show, Bounded, Enum) + +instance HasCodec TracePropagator where + codec = + ( boundedEnumCodec \case + B3 -> "b3" + TraceContext -> "tracecontext" + ) + "Possible trace propagators to use with OTLP" + +instance FromJSON TracePropagator where + parseJSON = J.withText "TracePropagator" \case + "b3" -> pure B3 + "tracecontext" -> pure TraceContext + x -> fail $ "unexpected string '" <> show x <> "'." + +instance ToJSON TracePropagator where + toJSON = \case + B3 -> J.String "b3" + TraceContext -> J.String "tracecontext" + defaultOtelExporterTracesEndpoint :: Maybe Text defaultOtelExporterTracesEndpoint = Nothing @@ -309,6 +349,9 @@ defaultOtelExporterHeaders = [] defaultOtelExporterResourceAttributes :: [NameValue] defaultOtelExporterResourceAttributes = [] +defaultOtelExporterTracesPropagators :: [TracePropagator] +defaultOtelExporterTracesPropagators = [B3] + -- https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#batching-processor newtype OtelBatchSpanProcessorConfig = OtelBatchSpanProcessorConfig { -- | The maximum batch size of every export. It must be smaller or equal to @@ -379,11 +422,13 @@ data OtelExporterInfo = OtelExporterInfo -- only operations on data are (1) folding and (2) union with a small -- map of default attributes, and Map should be is faster than HashMap for -- the latter. - _oteleiResourceAttributes :: Map Text Text + _oteleiResourceAttributes :: Map Text Text, + -- | Trace propagator to be used to extract and inject trace headers + _oteleiTracesPropagator :: Tracing.Propagator RequestHeaders ResponseHeaders } emptyOtelExporterInfo :: OtelExporterInfo -emptyOtelExporterInfo = OtelExporterInfo Nothing Nothing mempty +emptyOtelExporterInfo = OtelExporterInfo Nothing Nothing mempty mempty -- | Batch processor configuration for trace export. -- @@ -408,6 +453,16 @@ getMaxExportBatchSize = _obspiMaxExportBatchSize getMaxQueueSize :: OtelBatchSpanProcessorInfo -> Int getMaxQueueSize = _obspiMaxQueueSize +mkOtelTracesPropagator :: [TracePropagator] -> Tracing.HttpPropagator +mkOtelTracesPropagator tps = foldMap toPropagator tps + where + toPropagator = \case + B3 -> Tracing.b3TraceContextPropagator + TraceContext -> Tracing.w3cTraceContextPropagator + +getOtelTracesPropagator :: OpenTelemetryInfo -> Tracing.HttpPropagator +getOtelTracesPropagator = _oteleiTracesPropagator . _otiExporterOtlp + -- | Defaults taken from -- https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#batching-processor defaultOtelBatchSpanProcessorInfo :: OtelBatchSpanProcessorInfo diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index 02f2d192f05..45474a5f209 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -39,7 +39,6 @@ import Data.Aeson.KeyMap qualified as KM import Data.Aeson.Types qualified as J import Data.ByteString.Builder qualified as BB import Data.ByteString.Char8 qualified as B8 -import Data.ByteString.Char8 qualified as Char8 import Data.ByteString.Lazy qualified as BL import Data.CaseInsensitive qualified as CI import Data.HashMap.Strict qualified as HashMap @@ -79,6 +78,7 @@ import Hasura.RQL.DDL.Schema import Hasura.RQL.DDL.Schema.Cache.Config import Hasura.RQL.Types.BackendType import Hasura.RQL.Types.Endpoint as EP +import Hasura.RQL.Types.OpenTelemetry (getOtelTracesPropagator) import Hasura.RQL.Types.Roles (adminRoleName, roleNameToTxt) import Hasura.RQL.Types.SchemaCache import Hasura.RQL.Types.Source @@ -302,40 +302,19 @@ mkSpockAction :: mkSpockAction appStateRef qErrEncoder qErrModifier apiHandler = do AppEnv {..} <- lift askAppEnv AppContext {..} <- liftIO $ getAppContext appStateRef + SchemaCache {..} <- liftIO $ getSchemaCache appStateRef req <- Spock.request let origHeaders = Wai.requestHeaders req ipAddress = Wai.getSourceFromFallback req pathInfo = Wai.rawPathInfo req + propagators = getOtelTracesPropagator scOpenTelemetryConfig -- Bytes are actually read from the socket here. Time this. (ioWaitTime, reqBody) <- withElapsedTime $ liftIO $ Wai.strictRequestBody req (requestId, headers) <- getRequestId origHeaders - tracingCtx <- liftIO do - -- B3 TraceIds can have a length of either 64 bits (16 hex chars) or 128 bits - -- (32 hex chars). For 64-bit TraceIds, we pad them with zeros on the left to - -- make them 128 bits long. - let traceIdMaybe = - lookup "X-B3-TraceId" headers >>= \rawTraceId -> - if - | Char8.length rawTraceId == 32 -> - Tracing.traceIdFromHex rawTraceId - | Char8.length rawTraceId == 16 -> - Tracing.traceIdFromHex $ Char8.replicate 16 '0' <> rawTraceId - | otherwise -> - Nothing - case traceIdMaybe of - Just traceId -> do - freshSpanId <- Tracing.randomSpanId - let parentSpanId = Tracing.spanIdFromHex =<< lookup "X-B3-SpanId" headers - samplingState = Tracing.samplingStateFromHeader $ lookup "X-B3-Sampled" headers - pure $ Tracing.TraceContext traceId freshSpanId parentSpanId samplingState - Nothing -> do - freshTraceId <- Tracing.randomTraceId - freshSpanId <- Tracing.randomSpanId - let samplingState = Tracing.samplingStateFromHeader $ lookup "X-B3-Sampled" headers - pure $ Tracing.TraceContext freshTraceId freshSpanId Nothing samplingState + tracingCtx <- liftIO $ Tracing.extract propagators headers let runTrace :: forall m1 a1. diff --git a/server/src-lib/Hasura/Tracing.hs b/server/src-lib/Hasura/Tracing.hs index 5a95927d90b..51c490865b7 100644 --- a/server/src-lib/Hasura/Tracing.hs +++ b/server/src-lib/Hasura/Tracing.hs @@ -3,9 +3,13 @@ module Hasura.Tracing (module Tracing) where import Hasura.Tracing.Class as Tracing import Hasura.Tracing.Context as Tracing import Hasura.Tracing.Monad as Tracing +import Hasura.Tracing.Propagator as Tracing +import Hasura.Tracing.Propagator.B3 as Tracing +import Hasura.Tracing.Propagator.W3CTraceContext as Tracing import Hasura.Tracing.Reporter as Tracing import Hasura.Tracing.Sampling as Tracing import Hasura.Tracing.TraceId as Tracing +import Hasura.Tracing.TraceState as Tracing import Hasura.Tracing.Utils as Tracing {- Note [Tracing] diff --git a/server/src-lib/Hasura/Tracing/Class.hs b/server/src-lib/Hasura/Tracing/Class.hs index f795ad2ddf1..50ce6024054 100644 --- a/server/src-lib/Hasura/Tracing/Class.hs +++ b/server/src-lib/Hasura/Tracing/Class.hs @@ -16,6 +16,7 @@ import Hasura.Prelude import Hasura.Tracing.Context import Hasura.Tracing.Sampling import Hasura.Tracing.TraceId +import Hasura.Tracing.TraceState qualified as TS -------------------------------------------------------------------------------- -- MonadTrace @@ -105,7 +106,7 @@ newTrace :: (MonadIO m, MonadTrace m) => SamplingPolicy -> Text -> m a -> m a newTrace policy name body = do traceId <- randomTraceId spanId <- randomSpanId - let context = TraceContext traceId spanId Nothing SamplingDefer + let context = TraceContext traceId spanId Nothing SamplingDefer TS.emptyTraceState newTraceWith context policy name body -- | Create a new span with a randomly-generated id. diff --git a/server/src-lib/Hasura/Tracing/Context.hs b/server/src-lib/Hasura/Tracing/Context.hs index 4feea564594..db063f0c86a 100644 --- a/server/src-lib/Hasura/Tracing/Context.hs +++ b/server/src-lib/Hasura/Tracing/Context.hs @@ -9,6 +9,7 @@ import Data.Aeson qualified as J import Hasura.Prelude import Hasura.Tracing.Sampling import Hasura.Tracing.TraceId +import Hasura.Tracing.TraceState (TraceState) -- | Any additional human-readable key-value pairs relevant to the execution of -- a span. @@ -30,7 +31,10 @@ data TraceContext = TraceContext { tcCurrentTrace :: TraceId, tcCurrentSpan :: SpanId, tcCurrentParent :: Maybe SpanId, - tcSamplingState :: SamplingState + tcSamplingState :: SamplingState, + -- Optional vendor-specific trace identification information across different distributed tracing systems. + -- It's used for the W3C Trace Context only https://www.w3.org/TR/trace-context/#tracestate-header + tcStateState :: TraceState } -- Should this be here? This implicitly ties Tracing to the name of fields in HTTP headers. diff --git a/server/src-lib/Hasura/Tracing/Propagator.hs b/server/src-lib/Hasura/Tracing/Propagator.hs new file mode 100644 index 00000000000..759d42cf149 --- /dev/null +++ b/server/src-lib/Hasura/Tracing/Propagator.hs @@ -0,0 +1,64 @@ +{-# OPTIONS_GHC -Wno-type-defaults #-} + +module Hasura.Tracing.Propagator + ( Propagator (..), + HttpPropagator, + extract, + inject, + ) +where + +import Control.Monad.IO.Class +import Hasura.Prelude +import Hasura.Tracing.Context +import Hasura.Tracing.Sampling (samplingStateFromHeader) +import Hasura.Tracing.TraceId +import Hasura.Tracing.TraceState (emptyTraceState) +import Network.HTTP.Types (RequestHeaders, ResponseHeaders) + +-- | A carrier is the medium used by Propagators to read values from and write values to. +-- Each specific Propagator type defines its expected carrier type, such as a string map or a byte array. +data Propagator inboundCarrier outboundCarrier = Propagator + { extractor :: inboundCarrier -> SpanId -> Maybe TraceContext, + injector :: TraceContext -> outboundCarrier -> outboundCarrier + } + +instance (Semigroup o) => Semigroup (Propagator i o) where + (Propagator lExtract lInject) <> (Propagator rExtract rInject) = + Propagator + { extractor = \i sid -> lExtract i sid <|> rExtract i sid, + injector = \c -> lInject c <> rInject c + } + +instance (Semigroup o) => Monoid (Propagator i o) where + mempty = Propagator (\_ _ -> Nothing) (\_ p -> p) + +type HttpPropagator = Propagator RequestHeaders ResponseHeaders + +-- | Extracts the value from an incoming request. For example, from the headers of an HTTP request. +-- +-- If a value can not be parsed from the carrier, for a cross-cutting concern, the implementation MUST NOT throw an exception and MUST NOT store a new value in the Context, in order to preserve any previously existing valid value. +extract :: + (MonadIO m) => + Propagator i o -> + -- | The carrier that holds the propagation fields. For example, an incoming message or HTTP request. + i -> + -- | a new Context derived from the Context passed as argument, containing the extracted value, which can be a SpanContext, Baggage or another cross-cutting concern context. + m TraceContext +extract (Propagator extractor _) i = do + freshSpanId <- randomSpanId + onNothing (extractor i freshSpanId) (randomContext freshSpanId) + where + randomContext freshSpanId = do + freshTraceId <- randomTraceId + let samplingState = samplingStateFromHeader Nothing + pure $ TraceContext freshTraceId freshSpanId Nothing samplingState emptyTraceState + +-- | Injects the value into a carrier. For example, into the headers of an HTTP request. +inject :: + Propagator i o -> + TraceContext -> + -- | The carrier that holds the propagation fields. For example, an outgoing message or HTTP request. + o -> + o +inject (Propagator _ injector) c = injector c diff --git a/server/src-lib/Hasura/Tracing/Propagator/B3.hs b/server/src-lib/Hasura/Tracing/Propagator/B3.hs new file mode 100644 index 00000000000..e6c50d8efb7 --- /dev/null +++ b/server/src-lib/Hasura/Tracing/Propagator/B3.hs @@ -0,0 +1,49 @@ +-- | B3 Propagation is a specification for the header "b3" and those that start with "x-b3-". +-- These headers are used for trace context propagation across service boundaries. +-- https://github.com/openzipkin/b3-propagation +-- https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md#b3-requirements +module Hasura.Tracing.Propagator.B3 + ( b3TraceContextPropagator, + ) +where + +import Data.ByteString.Char8 qualified as Char8 +import Hasura.Prelude +import Hasura.Tracing.Context (TraceContext (..)) +import Hasura.Tracing.Propagator (Propagator (..)) +import Hasura.Tracing.Sampling (samplingStateFromHeader, samplingStateToHeader) +import Hasura.Tracing.TraceId +import Hasura.Tracing.TraceState (emptyTraceState) +import Network.HTTP.Client.Transformable (RequestHeaders, ResponseHeaders) + +b3TraceContextPropagator :: Propagator RequestHeaders ResponseHeaders +b3TraceContextPropagator = + Propagator + { extractor = extractB3TraceContext, + injector = \TraceContext {..} headers -> + headers + ++ catMaybes + [ Just ("X-B3-TraceId", traceIdToHex tcCurrentTrace), + Just ("X-B3-SpanId", spanIdToHex tcCurrentSpan), + ("X-B3-ParentSpanId",) . spanIdToHex <$> tcCurrentParent, + ("X-B3-Sampled",) <$> samplingStateToHeader tcSamplingState + ] + } + +extractB3TraceContext :: RequestHeaders -> SpanId -> Maybe TraceContext +extractB3TraceContext headers freshSpanId = do + -- B3 TraceIds can have a length of either 64 bits (16 hex chars) or 128 bits + -- (32 hex chars). For 64-bit TraceIds, we pad them with zeros on the left to + -- make them 128 bits long. + traceId <- + lookup "X-B3-TraceId" headers >>= \rawTraceId -> + if + | Char8.length rawTraceId == 32 -> + traceIdFromHex rawTraceId + | Char8.length rawTraceId == 16 -> + traceIdFromHex $ Char8.replicate 16 '0' <> rawTraceId + | otherwise -> + Nothing + let parentSpanId = spanIdFromHex =<< lookup "X-B3-SpanId" headers + samplingState = samplingStateFromHeader $ lookup "X-B3-Sampled" headers + Just $ TraceContext traceId freshSpanId parentSpanId samplingState emptyTraceState diff --git a/server/src-lib/Hasura/Tracing/Propagator/W3CTraceContext.hs b/server/src-lib/Hasura/Tracing/Propagator/W3CTraceContext.hs new file mode 100644 index 00000000000..65d2de283c2 --- /dev/null +++ b/server/src-lib/Hasura/Tracing/Propagator/W3CTraceContext.hs @@ -0,0 +1,129 @@ +-- | This module provides support for tracing context propagation in accordance with the W3C tracing context +-- propagation specifications: https://www.w3.org/TR/trace-context/ +module Hasura.Tracing.Propagator.W3CTraceContext + ( w3cTraceContextPropagator, + ) +where + +import Data.Attoparsec.ByteString.Char8 (Parser, hexadecimal, parseOnly, string, takeWhile) +import Data.Bits (Bits (setBit, testBit)) +import Data.ByteString (ByteString) +import Data.ByteString.Builder qualified as B +import Data.ByteString.Lazy qualified as L +import Data.Char (isHexDigit) +import Data.Text qualified as T +import Data.Word (Word8) +import Hasura.Prelude hiding (takeWhile) +import Hasura.Tracing.Context (TraceContext (..)) +import Hasura.Tracing.Propagator (Propagator (..)) +import Hasura.Tracing.Sampling (SamplingState (SamplingAccept, SamplingDefer)) +import Hasura.Tracing.TraceId +import Hasura.Tracing.TraceState (decodeTraceStateHeader) +import Hasura.Tracing.TraceState qualified as TS +import Network.HTTP.Types (RequestHeaders, ResponseHeaders) + +-- | Propagate trace context information via headers using the w3c specification format +w3cTraceContextPropagator :: Propagator RequestHeaders ResponseHeaders +w3cTraceContextPropagator = + Propagator + { extractor = \headers freshSpanId -> do + TraceParent {..} <- lookup "traceparent" headers >>= decodeTraceparentHeader + let traceState = lookup "tracestate" headers >>= decodeTraceStateHeader + Just + $ TraceContext + tpTraceId + freshSpanId + (Just tpParentId) + (traceFlagsToSampling tpTraceFlags) + (fromMaybe TS.emptyTraceState traceState), + injector = \context headers -> + let (traceParent, traceState) = encodeSpanContext context + in headers + ++ [ ("traceparent", traceParent), + ("tracestate", traceState) + ] + } + +-------------------------------------------------------------------------------- +-- TraceParent + +-- | The traceparent HTTP header field identifies the incoming request in a tracing system. +-- https://w3c.github.io/trace-context/#traceparent-header +data TraceParent = TraceParent + { tpVersion :: {-# UNPACK #-} !Word8, + tpTraceId :: {-# UNPACK #-} !TraceId, + tpParentId :: {-# UNPACK #-} !SpanId, + tpTraceFlags :: {-# UNPACK #-} !TraceFlags + } + deriving (Show) + +-- | Contain details about the trace. Unlike TraceState values, TraceFlags are present in all traces. +-- The current version of the specification only supports a single flag called sampled. +newtype TraceFlags = TraceFlags Word8 + deriving (Show, Eq, Ord) + +-- | TraceFlags with the @sampled@ flag not set. This means that it is up to the +-- sampling configuration to decide whether or not to sample the trace. +defaultTraceFlags :: TraceFlags +defaultTraceFlags = TraceFlags 0 + +-- | Get the current bitmask for the @TraceFlags@, useful for serialization purposes. +traceFlagsValue :: TraceFlags -> Word8 +traceFlagsValue (TraceFlags flags) = flags + +-- | Will the trace associated with this @TraceFlags@ value be sampled? +isSampled :: TraceFlags -> Bool +isSampled (TraceFlags flags) = flags `testBit` 0 + +-- | Set the @sampled@ flag on the @TraceFlags@ +setSampled :: TraceFlags -> TraceFlags +setSampled (TraceFlags flags) = TraceFlags (flags `setBit` 0) + +traceFlagsToSampling :: TraceFlags -> SamplingState +traceFlagsToSampling = bool SamplingDefer SamplingAccept . isSampled + +traceFlagsFromSampling :: SamplingState -> TraceFlags +traceFlagsFromSampling = \case + SamplingAccept -> setSampled defaultTraceFlags + _ -> defaultTraceFlags + +-- | Encoded the given 'TraceContext' into a @traceparent@, @tracestate@ tuple. +encodeSpanContext :: TraceContext -> (ByteString, ByteString) +encodeSpanContext TraceContext {..} = (traceparent, tracestate) + where + traceparent = + L.toStrict + $ B.toLazyByteString + -- version + $ B.word8HexFixed 0 + <> B.char7 '-' + <> B.byteString (traceIdToHex tcCurrentTrace) + <> B.char7 '-' + <> B.byteString (spanIdToHex tcCurrentSpan) + <> B.char7 '-' + <> B.word8HexFixed (traceFlagsValue $ traceFlagsFromSampling tcSamplingState) + + tracestate = + txtToBs + $ T.intercalate "," + $ (\(TS.Key key, TS.Value value) -> key <> "=" <> value) + <$> (TS.toTraceStateList tcStateState) + +traceparentParser :: Parser TraceParent +traceparentParser = do + tpVersion <- hexadecimal + _ <- string "-" + traceIdBs <- takeWhile isHexDigit + tpTraceId <- onNothing (traceIdFromHex traceIdBs) (fail "TraceId must be 8 bytes long") + _ <- string "-" + parentIdBs <- takeWhile isHexDigit + tpParentId <- onNothing (spanIdFromHex parentIdBs) (fail "ParentId must be 8 bytes long") + _ <- string "-" + tpTraceFlags <- TraceFlags <$> hexadecimal + -- Intentionally not consuming end of input in case of version > 0 + pure $ TraceParent {..} + +decodeTraceparentHeader :: ByteString -> Maybe TraceParent +decodeTraceparentHeader tp = case parseOnly traceparentParser tp of + Left _ -> Nothing + Right ok -> Just ok diff --git a/server/src-lib/Hasura/Tracing/Sampling.hs b/server/src-lib/Hasura/Tracing/Sampling.hs index 2111883ac12..9cd17bd5fb3 100644 --- a/server/src-lib/Hasura/Tracing/Sampling.hs +++ b/server/src-lib/Hasura/Tracing/Sampling.hs @@ -27,6 +27,7 @@ import System.Random.Stateful qualified as Random -- -- Debug sampling state not represented. data SamplingState = SamplingDefer | SamplingDeny | SamplingAccept + deriving (Show, Eq) -- | Convert a sampling state to a value for the X-B3-Sampled header. A return -- value of Nothing indicates that the header should not be set. diff --git a/server/src-lib/Hasura/Tracing/TraceState.hs b/server/src-lib/Hasura/Tracing/TraceState.hs new file mode 100644 index 00000000000..1dfd3e3e0ab --- /dev/null +++ b/server/src-lib/Hasura/Tracing/TraceState.hs @@ -0,0 +1,54 @@ +module Hasura.Tracing.TraceState + ( TraceState, + Key (..), + Value (..), + emptyTraceState, + toTraceStateList, + decodeTraceStateHeader, + ) +where + +import Data.Attoparsec.ByteString.Char8 (Parser, parseOnly, string, takeWhile, try) +import Data.ByteString (ByteString) +import Data.Char (isAsciiLower, isDigit) +import Hasura.Prelude hiding (empty, takeWhile, toList) + +newtype Key = Key Text + deriving (Show, Eq, Ord) + +newtype Value = Value Text + deriving (Show, Eq, Ord) + +-- | Data structure compliant with the storage and serialization needs of the W3C tracestate header. +-- https://www.w3.org/TR/trace-context/#tracestate-header +newtype TraceState = TraceState [(Key, Value)] + deriving (Show, Eq, Ord) + +-- | An empty 'TraceState' key-value pair dictionary +emptyTraceState :: TraceState +emptyTraceState = TraceState [] + +-- | Convert the 'TraceState' to a list. +toTraceStateList :: TraceState -> [(Key, Value)] +toTraceStateList (TraceState ts) = ts + +traceStateParser :: Parser TraceState +traceStateParser = do + pairs <- many stateItemParser + pure $ TraceState pairs + where + isValid c = isDigit c || (isAsciiLower c) + -- The tracestate field value is a list of list-members separated by commas (,) + -- e.g. vendorname1=opaqueValue1,vendorname2=opaqueValue2 + stateItemParser :: Parser (Key, Value) + stateItemParser = do + key <- takeWhile isValid + _ <- string "=" + value <- takeWhile isValid + _ <- try $ string "," + pure $ (Key (bsToTxt key), Value (bsToTxt value)) + +decodeTraceStateHeader :: Data.ByteString.ByteString -> Maybe TraceState +decodeTraceStateHeader ts = case parseOnly traceStateParser ts of + Left _ -> Nothing + Right ok -> Just ok diff --git a/server/src-lib/Hasura/Tracing/Utils.hs b/server/src-lib/Hasura/Tracing/Utils.hs index 1517d5d1f55..bafbf491afd 100644 --- a/server/src-lib/Hasura/Tracing/Utils.hs +++ b/server/src-lib/Hasura/Tracing/Utils.hs @@ -15,8 +15,7 @@ import Hasura.Prelude import Hasura.RQL.Types.SourceConfiguration (HasSourceConfiguration (..)) import Hasura.Tracing.Class import Hasura.Tracing.Context -import Hasura.Tracing.Sampling -import Hasura.Tracing.TraceId +import Hasura.Tracing.Propagator (HttpPropagator, inject) import Network.HTTP.Client.Transformable qualified as HTTP -- | Wrap the execution of an HTTP request in a span in the current @@ -28,12 +27,13 @@ import Network.HTTP.Client.Transformable qualified as HTTP -- created span, and injects the trace context into the HTTP header. traceHTTPRequest :: (MonadIO m, MonadTrace m) => + HttpPropagator -> -- | http request that needs to be made HTTP.Request -> -- | a function that takes the traced request and executes it (HTTP.Request -> m a) -> m a -traceHTTPRequest req f = do +traceHTTPRequest propagator req f = do let method = bsToTxt (view HTTP.method req) uri = view HTTP.url req newSpan (method <> " " <> uri) do @@ -43,13 +43,7 @@ traceHTTPRequest req f = do f $ over HTTP.headers (headers <>) req where toHeaders :: TraceContext -> [HTTP.Header] - toHeaders TraceContext {..} = - catMaybes - [ Just ("X-B3-TraceId", traceIdToHex tcCurrentTrace), - Just ("X-B3-SpanId", spanIdToHex tcCurrentSpan), - ("X-B3-ParentSpanId",) . spanIdToHex <$> tcCurrentParent, - ("X-B3-Sampled",) <$> samplingStateToHeader tcSamplingState - ] + toHeaders context = inject propagator context [] attachSourceConfigAttributes :: forall b m. (HasSourceConfiguration b, MonadTrace m) => SourceConfig b -> m () attachSourceConfigAttributes sourceConfig = do diff --git a/server/src-test/Hasura/Tracing/PropagatorSpec.hs b/server/src-test/Hasura/Tracing/PropagatorSpec.hs new file mode 100644 index 00000000000..f15da9c6ade --- /dev/null +++ b/server/src-test/Hasura/Tracing/PropagatorSpec.hs @@ -0,0 +1,80 @@ +module Hasura.Tracing.PropagatorSpec (spec) where + +import Data.Maybe (fromJust) +import Hasura.Prelude +import Hasura.RQL.Types.OpenTelemetry qualified as OTEL +import Hasura.Tracing +import Test.Hspec + +spec :: Spec +spec = do + describe "B3TraceContextPropagator" $ do + it "extract and inject x-b3 headers" $ do + traceId <- randomTraceId + spanId <- randomSpanId + parentSpanId <- randomSpanId + tc <- + extract + b3TraceContextPropagator + [ ("X-B3-TraceId", traceIdToHex traceId), + ("X-B3-SpanId", spanIdToHex spanId), + ("X-B3-ParentSpanId", spanIdToHex parentSpanId), + ("X-B3-Sampled", fromJust $ samplingStateToHeader SamplingAccept) + ] + tcCurrentTrace tc `shouldBe` traceId + tcCurrentParent tc `shouldBe` Just spanId + tcSamplingState tc `shouldBe` SamplingAccept + tcStateState tc `shouldBe` emptyTraceState + + inject b3TraceContextPropagator tc [] + `shouldBe` [ ("X-B3-TraceId", traceIdToHex traceId), + ("X-B3-SpanId", spanIdToHex $ tcCurrentSpan tc), + ("X-B3-ParentSpanId", spanIdToHex spanId), + ("X-B3-Sampled", fromJust $ samplingStateToHeader SamplingAccept) + ] + + describe "W3cTraceContextPropagator" $ do + it "extract and inject w3c tracecontext headers" $ do + traceId <- randomTraceId + spanId <- randomSpanId + parentSpanId <- randomSpanId + let headers = + inject + w3cTraceContextPropagator + (TraceContext traceId spanId (Just parentSpanId) SamplingAccept emptyTraceState) + [] + tc <- extract w3cTraceContextPropagator headers + tcCurrentTrace tc `shouldBe` traceId + tcCurrentParent tc `shouldBe` Just spanId + tcSamplingState tc `shouldBe` SamplingAccept + tcStateState tc `shouldBe` emptyTraceState + + describe "Composite Propagator" $ do + it "extract and inject propagator b3 + w3c" $ do + traceId1 <- randomTraceId + spanId1 <- randomSpanId + parentSpanId1 <- randomSpanId + traceId2 <- randomTraceId + spanId2 <- randomSpanId + parentSpanId2 <- randomSpanId + let propagator = OTEL.mkOtelTracesPropagator [OTEL.TraceContext, OTEL.B3] + headers = + ( inject + b3TraceContextPropagator + (TraceContext traceId1 spanId1 (Just parentSpanId1) SamplingAccept emptyTraceState) + [] + ) + <> ( inject + w3cTraceContextPropagator + (TraceContext traceId2 spanId2 (Just parentSpanId2) SamplingDefer emptyTraceState) + [] + ) + tc <- extract propagator headers + tcCurrentTrace tc `shouldBe` traceId2 + tcCurrentParent tc `shouldBe` Just spanId2 + tcSamplingState tc `shouldBe` SamplingDefer + tcStateState tc `shouldBe` emptyTraceState + + inject propagator tc [] + `shouldBe` (inject w3cTraceContextPropagator tc []) + <> (inject b3TraceContextPropagator tc [])