diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index e57609c718..8f1245c7e3 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -46,6 +46,10 @@ console = ["tokio/tracing", "console-subscriber"] # See https://github.com/apollographql/federation-rs/pull/185 docs_rs = ["router-bridge/docs_rs"] +# Enables the use of new telemetry features that are under development +# and not yet ready for production use. +telemetry_next = [] + [package.metadata.docs.rs] features = ["docs_rs"] diff --git a/apollo-router/src/plugins/telemetry/config.rs b/apollo-router/src/plugins/telemetry/config.rs index caf95203c3..a5956aabfe 100644 --- a/apollo-router/src/plugins/telemetry/config.rs +++ b/apollo-router/src/plugins/telemetry/config.rs @@ -56,12 +56,27 @@ pub(crate) struct Conf { /// Logging configuration #[serde(rename = "experimental_logging", default)] pub(crate) logging: Logging, + + #[cfg(feature = "telemetry_next")] + #[serde(rename = "logging", default)] + #[allow(dead_code)] + pub(crate) new_logging: config_new::logging::Logging, /// Metrics configuration pub(crate) metrics: Metrics, /// Tracing configuration pub(crate) tracing: Tracing, /// Apollo reporting configuration pub(crate) apollo: apollo::Config, + + #[cfg(feature = "telemetry_next")] + /// Event configuration + pub(crate) events: config_new::events::Events, + #[cfg(feature = "telemetry_next")] + /// Span configuration + pub(crate) spans: config_new::spans::Spans, + #[cfg(feature = "telemetry_next")] + /// Instrument configuration + pub(crate) instruments: config_new::instruments::Instruments, } /// Metrics configuration diff --git a/apollo-router/src/plugins/telemetry/config_new/attributes.rs b/apollo-router/src/plugins/telemetry/config_new/attributes.rs new file mode 100644 index 0000000000..05dce00d7c --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/attributes.rs @@ -0,0 +1,698 @@ +use std::collections::HashMap; + +use schemars::gen::SchemaGenerator; +use schemars::schema::Schema; +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::plugins::telemetry::config::AttributeValue; + +/// This struct can be used as an attributes container, it has a custom JsonSchema implementation that will merge the schemas of the attributes and custom fields. +#[allow(dead_code)] +#[derive(Clone, Deserialize, Debug)] +#[serde(default)] +pub(crate) struct Extendable +where + A: Default, +{ + #[serde(flatten)] + attributes: A, + + #[serde(flatten)] + custom: HashMap, +} + +impl Extendable<(), ()> { + pub(crate) fn empty() -> Extendable + where + A: Default, + { + Default::default() + } +} + +impl JsonSchema for Extendable +where + A: Default + JsonSchema, + E: JsonSchema, +{ + fn schema_name() -> String { + "extendable_attribute".to_string() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + let mut attributes = gen.subschema_for::(); + let custom = gen.subschema_for::>(); + if let Schema::Object(schema) = &mut attributes { + if let Some(object) = &mut schema.object { + object.additional_properties = + custom.into_object().object().additional_properties.clone(); + } + } + + attributes + } +} + +impl Default for Extendable +where + A: Default, +{ + fn default() -> Self { + Self { + attributes: Default::default(), + custom: HashMap::new(), + } + } +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum RouterEvent { + /// When a service request occurs. + Request, + /// When a service response occurs. + Response, + /// When a service error occurs. + Error, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug, Default)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum DefaultAttributeRequirementLevel { + /// Attributes that are marked as required in otel semantic conventions and apollo documentation will be included (default) + #[default] + Required, + /// Attributes that are marked as required or recommended in otel semantic conventions and apollo documentation will be included + Recommended, + /// Attributes that are marked as required, recommended or opt-in in otel semantic conventions and apollo documentation will be included + OptIn, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum TraceIdFormat { + /// Open Telemetry trace ID, a hex string. + OpenTelemetry, + /// Datadog trace ID, a u64. + Datadog, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, untagged)] +pub(crate) enum RouterCustomAttribute { + /// A header from the request + RequestHeader { + /// The name of the request header. + request_header: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + /// A header from the response + ResponseHeader { + /// The name of the request header. + response_header: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + /// The trace ID of the request. + TraceId { + /// The format of the trace ID. + trace_id: TraceIdFormat, + }, + /// A value from context. + ResponseContext { + /// The response context key. + response_context: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + /// A value from baggage. + Baggage { + /// The name of the baggage item. + baggage: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + /// A value from an environment variable. + Env { + /// The name of the environment variable + env: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, +} +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum OperationName { + /// The raw operation name. + String, + /// A hash of the operation name. + Hash, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum OperationKind { + /// The raw operation kind. + String, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, untagged)] +pub(crate) enum SupergraphCustomAttribute { + OperationName { + /// The operation name from the query. + operation_name: OperationName, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + OperationKind { + /// The operation kind from the query (query|mutation|subscription). + operation_kind: OperationKind, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + QueryVariable { + /// The name of a graphql query variable. + query_variable: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + ResponseBody { + /// Json Path into the response body + response_body: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + RequestHeader { + /// The name of the request header. + request_header: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + ResponseHeader { + /// The name of the response header. + response_header: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + RequestContext { + /// The request context key. + request_context: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + ResponseContext { + /// The response context key. + response_context: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + Baggage { + /// The name of the baggage item. + baggage: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + Env { + /// The name of the environment variable + env: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case", untagged)] +pub(crate) enum SubgraphCustomAttribute { + SubgraphOperationName { + /// The operation name from the subgraph query. + subgraph_operation_name: OperationName, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SubgraphOperationKind { + /// The kind of the subgraph operation (query|mutation|subscription). + subgraph_operation_kind: OperationKind, + }, + SubgraphQueryVariable { + /// The name of a subgraph query variable. + subgraph_query_variable: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SubgraphResponseBody { + /// The subgraph response body json path. + subgraph_response_body: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SubgraphRequestHeader { + /// The name of the subgraph request header. + subgraph_request_header: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SubgraphResponseHeader { + /// The name of the subgraph response header. + subgraph_response_header: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + + SupergraphOperationName { + /// The supergraph query operation name. + supergraph_operation_name: OperationName, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SupergraphOperationKind { + /// The supergraph query operation kind (query|mutation|subscription). + supergraph_operation_kind: OperationKind, + }, + SupergraphQueryVariable { + /// The supergraph query variable name. + supergraph_query_variable: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SupergraphResponseBody { + /// The supergraph response body json path. + supergraph_response_body: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SupergraphRequestHeader { + /// The supergraph request header name. + supergraph_request_header: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + SupergraphResponseHeader { + /// The supergraph response header name. + supergraph_response_header: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + RequestContext { + /// The request context key. + request_context: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + ResponseContext { + /// The response context key. + response_context: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + Baggage { + /// The name of the baggage item. + baggage: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, + Env { + /// The name of the environment variable + env: String, + /// Optional redaction pattern. + redact: Option, + /// Optional default value. + default: Option, + }, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(default)] +pub(crate) struct RouterAttributes { + /// Http attributes from Open Telemetry semantic conventions. + #[serde(flatten)] + common: HttpCommonAttributes, + /// Http server attributes from Open Telemetry semantic conventions. + #[serde(flatten)] + server: HttpServerAttributes, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(default)] +pub(crate) struct SupergraphAttributes { + /// The GraphQL document being executed. + /// Examples: + /// * query findBookById { bookById(id: ?) { name } } + /// Requirement level: Recommended + #[serde(rename = "graphql.document")] + graphql_document: Option, + /// The name of the operation being executed. + /// Examples: + /// * findBookById + /// Requirement level: Recommended + #[serde(rename = "graphql.operation.name")] + graphql_operation_name: Option, + /// The type of the operation being executed. + /// Examples: + /// * query + /// * subscription + /// * mutation + /// Requirement level: Recommended + #[serde(rename = "graphql.operation.type")] + graphql_operation_type: Option, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(default)] +pub(crate) struct SubgraphAttributes { + /// The name of the subgraph + /// Examples: + /// * products + /// Requirement level: Required + #[serde(rename = "graphql.federation.subgraph.name")] + graphql_federation_subgraph_name: Option, + /// The GraphQL document being executed. + /// Examples: + /// * query findBookById { bookById(id: ?) { name } } + /// Requirement level: Recommended + #[serde(rename = "graphql.document")] + graphql_document: Option, + /// The name of the operation being executed. + /// Examples: + /// * findBookById + /// Requirement level: Recommended + #[serde(rename = "graphql.operation.name")] + graphql_operation_name: Option, + /// The type of the operation being executed. + /// Examples: + /// * query + /// * subscription + /// * mutation + /// Requirement level: Recommended + #[serde(rename = "graphql.operation.type")] + graphql_operation_type: Option, +} + +/// Common attributes for http server and client. +/// See https://opentelemetry.io/docs/specs/semconv/http/http-spans/#common-attributes +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct HttpCommonAttributes { + /// Describes a class of error the operation ended with. + /// Examples: + /// * timeout + /// * name_resolution_error + /// * 500 + /// Requirement level: Conditionally Required: If request has ended with an error. + #[serde(rename = "error.type")] + error_type: Option, + + /// The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. + /// Examples: + /// * 3495 + /// Requirement level: Recommended + #[serde(rename = "http.request.body.size")] + http_request_body_size: Option, + + /// HTTP request method. + /// Examples: + /// * GET + /// * POST + /// * HEAD + /// Requirement level: Required + #[serde(rename = "http.request.method")] + http_request_method: Option, + + /// Original HTTP method sent by the client in the request line. + /// Examples: + /// * GeT + /// * ACL + /// * foo + /// Requirement level: Conditionally Required + #[serde(rename = "http.request.method.original")] + http_request_method_original: Option, + + /// The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the Content-Length header. For requests using transport encoding, this should be the compressed size. + /// Examples: + /// * 3495 + /// Requirement level: Recommended + #[serde(rename = "http.response.body.size")] + http_response_body_size: Option, + + /// HTTP response status code. + /// Examples: + /// * 200 + /// Requirement level: Conditionally Required: If and only if one was received/sent. + #[serde(rename = "http.response.status_code")] + http_response_status_code: Option, + + /// OSI application layer or non-OSI equivalent. + /// Examples: + /// * http + /// * spdy + /// Requirement level: Recommended: if not default (http). + #[serde(rename = "network.protocol.name")] + network_protocol_name: Option, + + /// Version of the protocol specified in network.protocol.name. + /// Examples: + /// * 1.0 + /// * 1.1 + /// * 2 + /// * 3 + /// Requirement level: Recommended + #[serde(rename = "network.protocol.version")] + network_protocol_version: Option, + + /// OSI transport layer. + /// Examples: + /// * tcp + /// * udp + /// Requirement level: Conditionally Required + #[serde(rename = "network.transport")] + network_transport: Option, + + /// OSI network layer or non-OSI equivalent. + /// Examples: + /// * ipv4 + /// * ipv6 + /// Requirement level: Recommended + #[serde(rename = "network.type")] + network_type: Option, + + /// Value of the HTTP User-Agent header sent by the client. + /// Examples: + /// * CERN-LineMode/2.15 + /// * libwww/2.17b3 + /// Requirement level: Recommended + #[serde(rename = "user_agent.original")] + user_agent_original: Option, +} + +/// Attributes for Http servers +/// See https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct HttpServerAttributes { + /// Client address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + /// Examples: + /// * 83.164.160.102 + /// Requirement level: Recommended + #[serde(rename = "client.address")] + client_address: Option, + /// The port of the original client behind all proxies, if known (e.g. from Forwarded or a similar header). Otherwise, the immediate client peer port. + /// Examples: + /// * 83.164.160.102 + /// Requirement level: Recommended + #[serde(rename = "client.port")] + client_port: Option, + /// The matched route (path template in the format used by the respective server framework). + /// Examples: + /// * 65123 + /// Requirement level: Conditionally Required: If and only if it’s available + #[serde(rename = "http.route")] + http_route: Option, + /// Local socket address. Useful in case of a multi-IP host. + /// Examples: + /// * 10.1.2.80 + /// * /tmp/my.sock + /// Requirement level: Opt-In + #[serde(rename = "network.local.address")] + network_local_address: Option, + /// Local socket port. Useful in case of a multi-port host. + /// Examples: + /// * 65123 + /// Requirement level: Opt-In + #[serde(rename = "network.local.port")] + network_local_port: Option, + /// Peer address of the network connection - IP address or Unix domain socket name. + /// Examples: + /// * 10.1.2.80 + /// * /tmp/my.sock + /// Requirement level: Recommended + #[serde(rename = "network.peer.address")] + network_peer_address: Option, + /// Peer port number of the network connection. + /// Examples: + /// * 65123 + /// Requirement level: Recommended + #[serde(rename = "network.peer.port")] + network_peer_port: Option, + /// Name of the local HTTP server that received the request. + /// Examples: + /// * example.com + /// * 10.1.2.80 + /// * /tmp/my.sock + /// Requirement level: Recommended + #[serde(rename = "server.address")] + server_address: Option, + /// Port of the local HTTP server that received the request. + /// Examples: + /// * 80 + /// * 8080 + /// * 443 + /// Requirement level: Recommended + #[serde(rename = "server.port")] + server_port: Option, + /// The URI path component + /// Examples: + /// * /search + /// Requirement level: Required + #[serde(rename = "url.path")] + url_path: Option, + /// The URI query component + /// Examples: + /// * q=OpenTelemetry + /// Requirement level: Conditionally Required: If and only if one was received/sent. + #[serde(rename = "url.query")] + url_query: Option, + + /// The URI scheme component identifying the used protocol. + /// Examples: + /// * http + /// * https + /// Requirement level: Required + #[serde(rename = "url.scheme")] + url_scheme: Option, +} + +/// Attrubtes for HTTP clients +/// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct HttpClientAttributes { + /// The ordinal number of request resending attempt. + /// Examples: + /// * + /// Requirement level: Recommended: if and only if request was retried. + #[serde(rename = "http.resend_count")] + http_resend_count: Option, + + /// Peer address of the network connection - IP address or Unix domain socket name. + /// Examples: + /// * 10.1.2.80 + /// * /tmp/my.sock + /// Requirement level: Recommended: If different than server.address. + #[serde(rename = "network.peer.address")] + network_peer_address: Option, + + /// Peer port number of the network connection. + /// Examples: + /// * 65123 + /// Requirement level: Recommended: If network.peer.address is set. + #[serde(rename = "network.peer.port")] + network_peer_port: Option, + + /// Host identifier of the “URI origin” HTTP request is sent to. + /// Examples: + /// * example.com + /// * 10.1.2.80 + /// * /tmp/my.sock + /// Requirement level: Required + #[serde(rename = "server.address")] + server_address: Option, + + /// Port identifier of the “URI origin” HTTP request is sent to. + /// Examples: + /// * 80 + /// * 8080 + /// * 433 + /// Requirement level: Conditionally Required + #[serde(rename = "server.port")] + server_port: Option, + + /// Absolute URL describing a network resource according to RFC3986 + /// Examples: + /// * https://www.foo.bar/search?q=OpenTelemetry#SemConv; + /// * localhost + /// Requirement level: Required + #[serde(rename = "url.full")] + url_full: Option, +} diff --git a/apollo-router/src/plugins/telemetry/config_new/events.rs b/apollo-router/src/plugins/telemetry/config_new/events.rs new file mode 100644 index 0000000000..b9fafcbcec --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/events.rs @@ -0,0 +1,89 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::plugins::telemetry::config_new::attributes::Extendable; +use crate::plugins::telemetry::config_new::attributes::RouterAttributes; +use crate::plugins::telemetry::config_new::attributes::RouterCustomAttribute; +use crate::plugins::telemetry::config_new::attributes::SubgraphAttributes; +use crate::plugins::telemetry::config_new::attributes::SubgraphCustomAttribute; +use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; +use crate::plugins::telemetry::config_new::attributes::SupergraphCustomAttribute; + +/// Events are +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct Events { + /// Router service events + router: Extendable>, + /// Subgraph service events + supergraph: + Extendable>, + /// Supergraph service events + subgraph: Extendable>, +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug, Default)] +#[serde(deny_unknown_fields, default)] +struct RouterEvents { + /// Log the router request + request: bool, + /// Log the router response + response: bool, + /// Log the router error + error: bool, +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug, Default)] +#[serde(deny_unknown_fields, default)] +struct SupergraphEvents { + /// Log the supergraph request + request: EventLevel, + /// Log the supergraph response + response: EventLevel, + /// Log the supergraph error + error: EventLevel, +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug, Default)] +#[serde(deny_unknown_fields, default)] +struct SubgraphEvents { + /// Log the subgraph request + request: EventLevel, + /// Log the subgraph response + response: EventLevel, + /// Log the subgraph error + error: EventLevel, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug, Default)] +#[serde(rename_all = "snake_case")] +pub(crate) enum EventLevel { + Info, + Warn, + Error, + #[default] + Off, +} + +/// An event that can be logged as part of a trace. +/// The event has an implicit `type` attribute that matches the name of the event in the yaml +/// and a message that can be used to provide additional information. +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug)] +pub(crate) struct Event +where + A: Default, +{ + /// The log level of the event. + level: EventLevel, + /// The event message. + message: String, + /// The event attributes. + #[serde(default = "Extendable::empty::")] + attributes: Extendable, +} diff --git a/apollo-router/src/plugins/telemetry/config_new/instruments.rs b/apollo-router/src/plugins/telemetry/config_new/instruments.rs new file mode 100644 index 0000000000..4d39d56bf9 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/instruments.rs @@ -0,0 +1,123 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; +use crate::plugins::telemetry::config_new::attributes::Extendable; +use crate::plugins::telemetry::config_new::attributes::RouterAttributes; +use crate::plugins::telemetry::config_new::attributes::RouterCustomAttribute; +use crate::plugins::telemetry::config_new::attributes::SubgraphAttributes; +use crate::plugins::telemetry::config_new::attributes::SubgraphCustomAttribute; +use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; +use crate::plugins::telemetry::config_new::attributes::SupergraphCustomAttribute; + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug, Default)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct Instruments { + /// The attributes to include by default in instruments based on their level as specified in the otel semantic conventions and Apollo documentation. + default_attribute_requirement_level: DefaultAttributeRequirementLevel, + + /// Router service instruments. For more information see documentation on Router lifecycle. + router: Extendable>, + /// Supergraph service instruments. For more information see documentation on Router lifecycle. + supergraph: Extendable< + SupergraphInstruments, + Instrument, + >, + /// Subgraph service instruments. For more information see documentation on Router lifecycle. + subgraph: + Extendable>, +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug, Default)] +#[serde(deny_unknown_fields, default)] +struct RouterInstruments { + /// Histogram of server request duration + #[serde(rename = "http.server.request.duration")] + http_server_request_duration: bool, + + /// Gauge of active requests + #[serde(rename = "http.server.active_requests")] + http_server_active_requests: bool, + + /// Histogram of server request body size + #[serde(rename = "http.server.request.body.size")] + http_server_request_body_size: bool, + + /// Histogram of server response body size + #[serde(rename = "http.server.response.body.size")] + http_server_response_body_size: bool, +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug, Default)] +#[serde(deny_unknown_fields, default)] +struct SupergraphInstruments {} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug, Default)] +#[serde(deny_unknown_fields, default)] +struct SubgraphInstruments { + /// Histogram of client request duration + #[serde(rename = "http.client.request.duration")] + http_client_request_duration: bool, + + /// Histogram of client request body size + #[serde(rename = "http.client.request.body.size")] + http_client_request_body_size: bool, + + /// Histogram of client response body size + #[serde(rename = "http.client.response.body.size")] + http_client_response_body_size: bool, +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug)] +pub(crate) struct Instrument +where + A: Default, +{ + /// The type of instrument. + #[serde(rename = "type")] + ty: InstrumentType, + + /// The value of the instrument. + value: InstrumentValue, + + /// The description of the instrument. + description: String, + + /// The units of the instrument, e.g. "ms", "bytes", "requests". + unit: String, + + /// Attributes to include on the instrument. + #[serde(default = "Extendable::empty::")] + attributes: Extendable, +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum InstrumentType { + /// A monotonic counter https://opentelemetry.io/docs/specs/otel/metrics/data-model/#sums + Counter, + + /// A counter https://opentelemetry.io/docs/specs/otel/metrics/data-model/#sums + UpDownCounter, + + /// A histogram https://opentelemetry.io/docs/specs/otel/metrics/data-model/#histogram + Histogram, + + /// A gauge https://opentelemetry.io/docs/specs/otel/metrics/data-model/#gauge + Gauge, +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize, JsonSchema, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum InstrumentValue { + Duration, + Unit, + Active, +} diff --git a/apollo-router/src/plugins/telemetry/config_new/logging.rs b/apollo-router/src/plugins/telemetry/config_new/logging.rs new file mode 100644 index 0000000000..f47567b2d9 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/logging.rs @@ -0,0 +1,96 @@ +use std::collections::BTreeMap; + +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::plugins::telemetry::config::AttributeValue; + +/// Logging configuration. +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct Logging { + /// Common configuration + pub(crate) common: LoggingCommon, + /// Settings for logging to stdout. + pub(crate) stdout: StdOut, + /// Settings for logging to a file. + pub(crate) file: File, +} + +#[derive(Clone, Debug, Deserialize, JsonSchema, Default)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct LoggingCommon { + /// Set a service.name resource in your metrics + pub(crate) service_name: Option, + /// Set a service.namespace attribute in your metrics + pub(crate) service_namespace: Option, + /// The Open Telemetry resource + pub(crate) resource: BTreeMap, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct StdOut { + /// Set to true to log to stdout. + pub(crate) enabled: bool, + /// The format to log to stdout. + pub(crate) format: Format, +} + +/// Log to a file +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct File { + /// Set to true to log to a file. + pub(crate) enabled: bool, + /// The path pattern of the file to log to. + pub(crate) path: String, + /// The format of the log file. + pub(crate) format: Format, + /// The period to rollover the log file. + pub(crate) rollover: Rollover, +} + +/// The format for logging. +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum Format { + /// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData-discoverable-fields.html + Aws, + /// https://github.com/trentm/node-bunyan + Bunyan, + /// https://go2docs.graylog.org/5-0/getting_in_log_data/ingest_gelf.html#:~:text=The%20Graylog%20Extended%20Log%20Format,UDP%2C%20TCP%2C%20or%20HTTP. + Gelf, + + /// https://cloud.google.com/logging/docs/structured-logging + Google, + /// https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-appender-log + OpenTelemetry, + + /// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/format/struct.Json.html + Json, + + /// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/format/struct.Full.html + #[default] + Text, +} + +/// The period to rollover the log file. +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub(crate) enum Rollover { + /// Roll over every minute. + Minutely, + /// Roll over every hour. + Hourly, + /// Roll over every day. + #[default] + Daily, + /// Never roll over. + Never, +} diff --git a/apollo-router/src/plugins/telemetry/config_new/mod.rs b/apollo-router/src/plugins/telemetry/config_new/mod.rs new file mode 100644 index 0000000000..d06a466de3 --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod attributes; +/// These modules contain a new config structure for telemetry that will progressively mo +pub(crate) mod events; +pub(crate) mod instruments; +pub(crate) mod logging; +pub(crate) mod spans; diff --git a/apollo-router/src/plugins/telemetry/config_new/spans.rs b/apollo-router/src/plugins/telemetry/config_new/spans.rs new file mode 100644 index 0000000000..adf8c848bd --- /dev/null +++ b/apollo-router/src/plugins/telemetry/config_new/spans.rs @@ -0,0 +1,82 @@ +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel; +use crate::plugins::telemetry::config_new::attributes::Extendable; +use crate::plugins::telemetry::config_new::attributes::HttpCommonAttributes; +use crate::plugins::telemetry::config_new::attributes::HttpServerAttributes; +use crate::plugins::telemetry::config_new::attributes::RouterCustomAttribute; +use crate::plugins::telemetry::config_new::attributes::SubgraphAttributes; +use crate::plugins::telemetry::config_new::attributes::SubgraphCustomAttribute; +use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes; +use crate::plugins::telemetry::config_new::attributes::SupergraphCustomAttribute; + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct Spans { + /// Whether to create a `request` span. This will be removed in future, and users should set this to false. + legacy_request_span: bool, + + /// The attributes to include by default in spans based on their level as specified in the otel semantic conventions and Apollo documentation. + default_attribute_requirement_level: DefaultAttributeRequirementLevel, + + /// Configuration of router spans. + /// Log events inherit attributes from the containing span, so attributes configured here will be included on log events for a request. + /// Router spans contain http request and response information and therefore contain http specific attributes. + router: RouterSpans, + + /// Configuration of supergraph spans. + /// Supergraph spans contain information about the graphql request and response and therefore contain graphql specific attributes. + supergraph: SupergraphSpans, + + /// Attributes to include on the subgraph span. + /// Subgraph spans contain information about the subgraph request and response and therefore contain subgraph specific attributes. + subgraph: SubgraphSpans, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug, Default)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct RouterSpans { + /// Custom attributes that are attached to the router span. + attributes: Extendable, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(default)] +pub(crate) struct RouterAttributes { + /// Attach the datadog trace ID to the router span as dd.trace_id. + /// This can be output in logs and used to correlate traces in Datadog. + #[serde(rename = "dd.trace_id")] + datadog_trace_id: Option, + + /// Attach the opentelemetry trace ID to the router span as trace_id. + /// This can be output in logs. + #[serde(rename = "trace_id")] + trace_id: Option, + + /// Span http attributes from Open Telemetry semantic conventions. + #[serde(flatten)] + common: HttpCommonAttributes, + /// Span http server attributes from Open Telemetry semantic conventions. + #[serde(flatten)] + server: HttpServerAttributes, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Debug, Default)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct SupergraphSpans { + /// Custom attributes that are attached to the supergraph span. + attributes: Extendable, +} + +#[allow(dead_code)] +#[derive(Deserialize, JsonSchema, Clone, Default, Debug)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct SubgraphSpans { + /// Custom attributes that are attached to the subgraph span. + attributes: Extendable, +} diff --git a/apollo-router/src/plugins/telemetry/mod.rs b/apollo-router/src/plugins/telemetry/mod.rs index 8ea8cdd4ab..4888e2a901 100644 --- a/apollo-router/src/plugins/telemetry/mod.rs +++ b/apollo-router/src/plugins/telemetry/mod.rs @@ -124,6 +124,7 @@ use crate::ListenAddr; pub(crate) mod apollo; pub(crate) mod apollo_exporter; pub(crate) mod config; +mod config_new; mod endpoint; pub(crate) mod formatters; pub(crate) mod metrics;