Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(telemetry): improve support of conditions at the request level, especially for events #5759

Merged
merged 15 commits into from
Aug 22, 2024
26 changes: 26 additions & 0 deletions .changesets/feat_candle_exhale_deodorant_weeds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
### Add warnings for invalid configuration on custom telemetry ([PR #5759](https://github.com/apollographql/router/issues/5759))

For example sometimes if you have configuration like this:

```yaml
telemetry:
instrumentation:
events:
subgraph:
my.event:
message: "Auditing Router Event"
level: info
on: request
attributes:
subgraph.response.status:
subgraph_response_status: code # This is a first warning because you can't access to the response if you're at the request stage
condition:
eq:
- subgraph_name # Another warning because instead of writing subgraph_name: true which is the selector, you're asking for a comparison between 2 strings ("subgraph_name" and "product")
- product
```

This configuration is syntaxically correct but wouldn't probably do what you would like to. I put comments to highlight 2 mistakes in this example.
Before it was silently computed, now you'll get warning when starting the router.

By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5759
21 changes: 21 additions & 0 deletions .changesets/fix_bnjjj_fix_5702.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
### Improve support of conditions at the request level, especially for events ([Issue #5702](https://github.com/apollographql/router/issues/5702))

`exists` condition is now properly handled with events, this configuration will now work:

```yaml
telemetry:
instrumentation:
events:
supergraph:
my.event:
message: "Auditing Router Event"
level: info
on: request
attributes:
graphql.operation.name: true
condition:
exists:
operation_name: string
```

By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/5759
8 changes: 8 additions & 0 deletions apollo-router/src/plugins/telemetry/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ pub(crate) struct Instrumentation {
pub(crate) instruments: config_new::instruments::InstrumentsConfig,
}

impl Instrumentation {
pub(crate) fn validate(&self) -> Result<(), String> {
self.events.validate()?;
self.instruments.validate()?;
self.spans.validate()
}
}

/// Metrics configuration
#[derive(Clone, Default, Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields, default)]
Expand Down
16 changes: 16 additions & 0 deletions apollo-router/src/plugins/telemetry/config_new/conditional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ where
}
}

impl<Att, Request, Response, EventResponse> Conditional<Att>
where
Att: Selector<Request = Request, Response = Response, EventResponse = EventResponse>,
{
pub(crate) fn validate(&self) -> Result<(), String> {
match &self.condition {
Some(cond) => cond.lock().validate(None),
None => Ok(()),
}
}
}

impl<Att, Request, Response, EventResponse> Selector for Conditional<Att>
where
Att: Selector<Request = Request, Response = Response, EventResponse = EventResponse>,
Expand Down Expand Up @@ -334,6 +346,10 @@ where
_ => None,
}
}

fn is_active(&self, stage: super::Stage) -> bool {
self.selector.is_active(stage)
}
}

/// Custom Deserializer for attributes that will deserialize into a custom field if possible, but otherwise into one of the pre-defined attributes.
Expand Down
159 changes: 138 additions & 21 deletions apollo-router/src/plugins/telemetry/config_new/conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use tower::BoxError;

use super::Stage;
use crate::plugins::telemetry::config::AttributeValue;
use crate::plugins::telemetry::config_new::Selector;
use crate::Context;
Expand Down Expand Up @@ -55,28 +56,98 @@ impl<T> Condition<T>
where
T: Selector,
{
pub(crate) fn evaluate_request(&mut self, request: &T::Request) -> Option<bool> {
/// restricted_stage is Some if this condiiton will only applies at a specific stage like for events for example
pub(crate) fn validate(&self, restricted_stage: Option<Stage>) -> Result<(), String> {
match self {
Condition::Eq(eq) => match (eq[0].on_request(request), eq[1].on_request(request)) {
(None, None) => None,
(None, Some(right)) => {
eq[1] = SelectorOrValue::Value(right.into());
None
Condition::Eq(arr) | Condition::Gt(arr) | Condition::Lt(arr) => match (&arr[0], &arr[1]) {
(SelectorOrValue::Value(val1), SelectorOrValue::Value(val2)) => {
Err(format!("trying to compare 2 values ('{val1}' and '{val2}'), usually it's a syntax error because you want to use a specific selector and a value in a condition"))
}
(Some(left), None) => {
eq[0] = SelectorOrValue::Value(left.into());
None
}
(Some(left), Some(right)) => {
if left == right {
*self = Condition::True;
Some(true)
} else {
Some(false)
(SelectorOrValue::Value(_), SelectorOrValue::Selector(sel)) | (SelectorOrValue::Selector(sel), SelectorOrValue::Value(_)) => {
// Special condition for events
if let Some(Stage::Request) = &restricted_stage {
if !sel.is_active(Stage::Request) {
return Err(format!("selector {sel:?} is only valid for request stage, this log event will never trigger"));
}
}
Ok(())
},
(SelectorOrValue::Selector(sel1), SelectorOrValue::Selector(sel2)) => {
// Special condition for events
if let Some(Stage::Request) = &restricted_stage {
if !sel1.is_active(Stage::Request) {
return Err(format!("selector {sel1:?} is only valid for request stage, this log event will never trigger"));
}
if !sel2.is_active(Stage::Request) {
return Err(format!("selector {sel2:?} is only valid for request stage, this log event will never trigger"));
}
}
Ok(())
},
},
Condition::Exists(sel) => {
match restricted_stage {
Some(stage) => {
if sel.is_active(stage) {
Ok(())
} else {
Err(format!("the 'exists' condition use a selector applied at the wrong stage, this condition will be executed at the {} stage", stage))
}
},
None => Ok(())
}
},
Condition::All(all) => {
for cond in all {
cond.validate(restricted_stage)?;
}

Ok(())
},
Condition::Any(any) => {
for cond in any {
cond.validate(restricted_stage)?;
}

Ok(())
},
Condition::Not(cond) => cond.validate(restricted_stage),
Condition::True | Condition::False => Ok(()),
}
}

pub(crate) fn evaluate_request(&mut self, request: &T::Request) -> Option<bool> {
match self {
Condition::Eq(eq) => {
if !eq[0].is_active(Stage::Request) && !eq[1].is_active(Stage::Request) {
// Nothing to compute here
return None;
}
match (eq[0].on_request(request), eq[1].on_request(request)) {
(None, None) => None,
(None, Some(right)) => {
eq[1] = SelectorOrValue::Value(right.into());
None
}
(Some(left), None) => {
eq[0] = SelectorOrValue::Value(left.into());
None
}
(Some(left), Some(right)) => {
if left == right {
*self = Condition::True;
Some(true)
} else {
Some(false)
}
}
}
}
Condition::Gt(gt) => {
if !gt[0].is_active(Stage::Request) && !gt[1].is_active(Stage::Request) {
// Nothing to compute here
return None;
}
let left_att = gt[0].on_request(request).map(AttributeValue::from);
let right_att = gt[1].on_request(request).map(AttributeValue::from);
match (left_att, right_att) {
Expand Down Expand Up @@ -112,6 +183,10 @@ where
}
}
Condition::Lt(lt) => {
if !lt[0].is_active(Stage::Request) && !lt[1].is_active(Stage::Request) {
// Nothing to compute here
return None;
}
let left_att = lt[0].on_request(request).map(AttributeValue::from);
let right_att = lt[1].on_request(request).map(AttributeValue::from);
match (left_att, right_att) {
Expand Down Expand Up @@ -147,9 +222,13 @@ where
}
}
Condition::Exists(exist) => {
if exist.on_request(request).is_some() {
*self = Condition::True;
Some(true)
if exist.is_active(Stage::Request) {
if exist.on_request(request).is_some() {
*self = Condition::True;
Some(true)
} else {
Some(false)
}
} else {
None
}
Expand Down Expand Up @@ -331,6 +410,7 @@ where
Condition::False => false,
}
}

pub(crate) fn evaluate_drop(&self) -> Option<bool> {
match self {
Condition::Eq(eq) => match (eq[0].on_drop(), eq[1].on_drop()) {
Expand Down Expand Up @@ -478,6 +558,13 @@ where
SelectorOrValue::Selector(selector) => selector.on_drop(),
}
}

fn is_active(&self, stage: super::Stage) -> bool {
match self {
SelectorOrValue::Value(_) => true,
SelectorOrValue::Selector(selector) => selector.is_active(stage),
}
}
}

#[cfg(test)]
Expand All @@ -494,8 +581,10 @@ mod test {
use crate::plugins::telemetry::config_new::test::field;
use crate::plugins::telemetry::config_new::test::ty;
use crate::plugins::telemetry::config_new::Selector;
use crate::plugins::telemetry::config_new::Stage;
use crate::Context;

#[derive(Debug)]
enum TestSelector {
Req,
Resp,
Expand Down Expand Up @@ -567,11 +656,22 @@ mod test {
_ => None,
}
}

fn is_active(&self, stage: crate::plugins::telemetry::config_new::Stage) -> bool {
match self {
Req => matches!(stage, Stage::Request),
Resp => matches!(
stage,
Stage::Response | Stage::ResponseEvent | Stage::ResponseField
),
Static(_) => true,
}
}
}

#[test]
fn test_condition_exist() {
assert_eq!(exists(Req).req(None), None);
assert_eq!(exists(Req).req(None), Some(false));
assert_eq!(exists(Req).req(Some(1i64)), Some(true));
assert!(!exists(Resp).resp(None));
assert!(exists(Resp).resp(Some(1i64)));
Expand Down Expand Up @@ -747,7 +847,8 @@ mod test {
assert_eq!(lt(Req, Req).req(None), None);

assert_eq!(exists(Req).req(Some(1i64)), Some(true));
assert_eq!(exists(Req).req(None), None);
assert_eq!(exists(Req).req(None), Some(false));
assert!(!exists(Resp).resp(None));

assert_eq!(all(eq(1, 1), eq(1, Req)).req(Some(1i64)), Some(true));
assert_eq!(all(eq(1, 1), eq(1, Req)).req(None), None);
Expand All @@ -760,6 +861,22 @@ mod test {
assert!(eq(Resp, "error").error(Some("error")));
}

#[test]
fn test_condition_validate() {
assert!(eq(Req, 1).validate(Some(Stage::Request)).is_ok());
assert!(eq(Req, 1).validate(Some(Stage::Response)).is_ok());
assert!(eq(1, Req).validate(Some(Stage::Request)).is_ok());
assert!(eq(1, Req).validate(Some(Stage::Response)).is_ok());
assert!(eq(Resp, 1).validate(Some(Stage::Request)).is_err());
assert!(eq(Resp, 1).validate(None).is_ok());
assert!(eq(1, Resp).validate(None).is_ok());
assert!(eq(1, Resp).validate(Some(Stage::Request)).is_err());
assert!(exists(Resp).validate(Some(Stage::Request)).is_err());
assert!(exists(Req).validate(None).is_ok());
assert!(exists(Req).validate(Some(Stage::Request)).is_ok());
assert!(exists(Resp).validate(None).is_ok());
}

#[test]
fn test_evaluate_drop() {
assert!(eq(Req, 1).evaluate_drop().is_none());
Expand Down
Loading
Loading