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

🌊 Add type safety to Painless conditions #202603

Merged

Conversation

simianhacker
Copy link
Member

@simianhacker simianhacker commented Dec 2, 2024

🍒 Summary

This PR closes https://github.com/elastic/streams-program/issues/18 by adding some basic type checking to the painless output for the Stream conditions. The new code will check to ensure that none of the fields used in the condition are Map objects. Then it wraps the if statement in a try/catch.

Condition

{
  and: [
    { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
    {
      or: [
        { field: 'log.level', operator: 'eq' as const, value: 'error' },
        { field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
      ],
    },
  ],
}

Before

(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))

After

if (ctx.log?.logger instanceof Map || ctx.log?.level instanceof Map) {
  return false;
}
try {
  if ((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))) {
    return true;
  }
  return false;
} catch (Exception e) {
  return false;
}

@simianhacker simianhacker added Feature:Streams This is the label for the Streams Project v9.0.0 backport:prev-minor Backport to (8.x) the previous minor version (i.e. one version back from main) v8.18.0 labels Dec 2, 2024
@simianhacker simianhacker changed the title Add type safety to Painless conditions 🌊 Add type safety to Painless conditions Dec 2, 2024
@flash1293
Copy link
Contributor

Thanks for setting this up, @simianhacker - works really well, the only thing I think would make sense to add is to cast to number types when doing gt, gte and so on both for values and fields.

Otherwise this situation will not work as I would expect:

// condition
{
            "field": "numberfield",
            "operator": "gt",
            "value": "15"
          }

// Doc (will not be rerouted)
POST logs/_doc
{
  "numberfield": "18"
}
// condition
{
            "field": "numberfield",
            "operator": "gt",
            "value": 15
          }

// Doc (will not be rerouted)
POST logs/_doc
{
  "numberfield": "18"
}
// condition
{
            "field": "numberfield",
            "operator": "gt",
            "value": "15"
          }

// Doc (will not be rerouted)
POST logs/_doc
{
  "numberfield": 18
}

@flash1293
Copy link
Contributor

The same thing we should probably do the other way for contains and equals so everything is a string always? The UI only gives one input field for the value, so we can't really tell whether we should do an equal check on strings or numbers.

@simianhacker
Copy link
Member Author

@flash1293 I added some type safety to lt, lte, gt, gte, contains, startsWith, and endsWith. I also added some integration tests. Let me know what you think.

@simianhacker simianhacker marked this pull request as ready for review December 4, 2024 02:11
@simianhacker simianhacker requested review from a team as code owners December 4, 2024 02:11
@flash1293
Copy link
Contributor

flash1293 commented Dec 4, 2024

Thanks for the changes @simianhacker , this works great, I just found one more case we should handle which is eq/neq:

// condition
{
            "field": "numberfield",
            "operator": "eq",
            "value": 15
          }

// Doc (will be rerouted)
POST logs/_doc
{
  "numberfield": 15
}

// Doc (will not be rerouted)
POST logs/_doc
{
  "numberfield": "15"
}

@simianhacker
Copy link
Member Author

@flash1293 I wonder if we shouldn't keep the eq and neq as EXACT match instead of converting it to a string. Maybe add regex and the caveat for using regex is that it's always converted to a string and then does the regex match.

@simianhacker simianhacker added the release_note:skip Skip the PR/issue when compiling release notes label Dec 4, 2024
@flash1293
Copy link
Contributor

@simianhacker how would the user use eq/neq in the UI?

This is how it looks today:
Screenshot 2024-12-04 at 19 16 56

Would the user need to enter "15" in order to match 15-the-string? That seems weird to me, especially because it's hard to tell how the data will arrive. Reaching to regex for this also seems weird.

This is not just a theoretical concern, it's pretty common for numbers to be sent as strings instead, but it's also not always the case.

I'm happy to solve this general concern in a different way, but I think we need to solve it somehow - casts just seem like the easiest way to do so. Maybe we need a "like" operator? :)

@simianhacker
Copy link
Member Author

@flash1293 I see you point, that's kind of a limitation of HTML forms is that it's really hard to discern a number vs a string when the API can accept both. Base on our Slack conversation, let's move towards "ease of use". The distinction in the API of "15" vs 15 feels like a "foot gun" for new users when it comes to eq and neq, they probably mean the same thing.

I'll add the same checks/conversion to eq and neq

@flash1293
Copy link
Contributor

The distinction in the API of "15" vs 15 feels like a "foot gun" for new users when it comes to eq and neq, they probably mean the same thing

Should we follow through and only allow strings on the API?

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

✅ unchanged

History

Copy link
Contributor

@flash1293 flash1293 left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for all the changes!

@simianhacker simianhacker merged commit fe56d6d into elastic:main Dec 5, 2024
9 checks passed
@kibanamachine
Copy link
Contributor

Starting backport for target branches: 8.x

https://github.com/elastic/kibana/actions/runs/12181236765

kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Dec 5, 2024
## 🍒  Summary

This PR closes elastic/streams-program#18 by
adding some basic type checking to the painless output for the Stream
conditions. The new code will check to ensure that none of the fields
used in the condition are `Map` objects. Then it wraps the if statement
in a `try/catch`.

### Condition
```Typescript
{
  and: [
    { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
    {
      or: [
        { field: 'log.level', operator: 'eq' as const, value: 'error' },
        { field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
      ],
    },
  ],
}
```

### Before

```
(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))
```

### After

```
if (ctx.log?.logger instanceof Map || ctx.log?.level instanceof Map) {
  return false;
}
try {
  if ((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))) {
    return true;
  }
  return false;
} catch (Exception e) {
  return false;
}
```

(cherry picked from commit fe56d6d)
@kibanamachine
Copy link
Contributor

💚 All backports created successfully

Status Branch Result
8.x

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

kibanamachine added a commit that referenced this pull request Dec 5, 2024
# Backport

This will backport the following commits from `main` to `8.x`:
- [🌊 Add type safety to Painless conditions
(#202603)](#202603)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Chris
Cowan","email":"chris@elastic.co"},"sourceCommit":{"committedDate":"2024-12-05T13:58:36Z","message":"🌊
Add type safety to Painless conditions (#202603)\n\n## 🍒
Summary\r\n\r\nThis PR closes
elastic/streams-program#18 by\r\nadding some
basic type checking to the painless output for the Stream\r\nconditions.
The new code will check to ensure that none of the fields\r\nused in the
condition are `Map` objects. Then it wraps the if statement\r\nin a
`try/catch`.\r\n\r\n### Condition\r\n```Typescript\r\n{\r\n and: [\r\n {
field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy'
},\r\n {\r\n or: [\r\n { field: 'log.level', operator: 'eq' as const,
value: 'error' },\r\n { field: 'log.level', operator: 'eq' as const,
value: 'ERROR' },\r\n ],\r\n },\r\n ],\r\n}\r\n```\r\n\r\n###
Before\r\n\r\n```\r\n(ctx.log?.logger !== null && ctx.log?.logger ==
\"nginx_proxy\") && ((ctx.log?.level !== null && ctx.log?.level ==
\"error\") || (ctx.log?.level !== null && ctx.log?.level ==
\"ERROR\"))\r\n```\r\n\r\n### After\r\n\r\n```\r\nif (ctx.log?.logger
instanceof Map || ctx.log?.level instanceof Map) {\r\n return
false;\r\n}\r\ntry {\r\n if ((ctx.log?.logger !== null &&
ctx.log?.logger == \"nginx_proxy\") && ((ctx.log?.level !== null &&
ctx.log?.level == \"error\") || (ctx.log?.level !== null &&
ctx.log?.level == \"ERROR\"))) {\r\n return true;\r\n }\r\n return
false;\r\n} catch (Exception e) {\r\n return
false;\r\n}\r\n```","sha":"fe56d6d90af45b971ff82731c1ec7eb4c4c0eff3","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","v8.18.0","Feature:Streams"],"title":"🌊
Add type safety to Painless
conditions","number":202603,"url":"https://github.com/elastic/kibana/pull/202603","mergeCommit":{"message":"🌊
Add type safety to Painless conditions (#202603)\n\n## 🍒
Summary\r\n\r\nThis PR closes
elastic/streams-program#18 by\r\nadding some
basic type checking to the painless output for the Stream\r\nconditions.
The new code will check to ensure that none of the fields\r\nused in the
condition are `Map` objects. Then it wraps the if statement\r\nin a
`try/catch`.\r\n\r\n### Condition\r\n```Typescript\r\n{\r\n and: [\r\n {
field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy'
},\r\n {\r\n or: [\r\n { field: 'log.level', operator: 'eq' as const,
value: 'error' },\r\n { field: 'log.level', operator: 'eq' as const,
value: 'ERROR' },\r\n ],\r\n },\r\n ],\r\n}\r\n```\r\n\r\n###
Before\r\n\r\n```\r\n(ctx.log?.logger !== null && ctx.log?.logger ==
\"nginx_proxy\") && ((ctx.log?.level !== null && ctx.log?.level ==
\"error\") || (ctx.log?.level !== null && ctx.log?.level ==
\"ERROR\"))\r\n```\r\n\r\n### After\r\n\r\n```\r\nif (ctx.log?.logger
instanceof Map || ctx.log?.level instanceof Map) {\r\n return
false;\r\n}\r\ntry {\r\n if ((ctx.log?.logger !== null &&
ctx.log?.logger == \"nginx_proxy\") && ((ctx.log?.level !== null &&
ctx.log?.level == \"error\") || (ctx.log?.level !== null &&
ctx.log?.level == \"ERROR\"))) {\r\n return true;\r\n }\r\n return
false;\r\n} catch (Exception e) {\r\n return
false;\r\n}\r\n```","sha":"fe56d6d90af45b971ff82731c1ec7eb4c4c0eff3"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202603","number":202603,"mergeCommit":{"message":"🌊
Add type safety to Painless conditions (#202603)\n\n## 🍒
Summary\r\n\r\nThis PR closes
elastic/streams-program#18 by\r\nadding some
basic type checking to the painless output for the Stream\r\nconditions.
The new code will check to ensure that none of the fields\r\nused in the
condition are `Map` objects. Then it wraps the if statement\r\nin a
`try/catch`.\r\n\r\n### Condition\r\n```Typescript\r\n{\r\n and: [\r\n {
field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy'
},\r\n {\r\n or: [\r\n { field: 'log.level', operator: 'eq' as const,
value: 'error' },\r\n { field: 'log.level', operator: 'eq' as const,
value: 'ERROR' },\r\n ],\r\n },\r\n ],\r\n}\r\n```\r\n\r\n###
Before\r\n\r\n```\r\n(ctx.log?.logger !== null && ctx.log?.logger ==
\"nginx_proxy\") && ((ctx.log?.level !== null && ctx.log?.level ==
\"error\") || (ctx.log?.level !== null && ctx.log?.level ==
\"ERROR\"))\r\n```\r\n\r\n### After\r\n\r\n```\r\nif (ctx.log?.logger
instanceof Map || ctx.log?.level instanceof Map) {\r\n return
false;\r\n}\r\ntry {\r\n if ((ctx.log?.logger !== null &&
ctx.log?.logger == \"nginx_proxy\") && ((ctx.log?.level !== null &&
ctx.log?.level == \"error\") || (ctx.log?.level !== null &&
ctx.log?.level == \"ERROR\"))) {\r\n return true;\r\n }\r\n return
false;\r\n} catch (Exception e) {\r\n return
false;\r\n}\r\n```","sha":"fe56d6d90af45b971ff82731c1ec7eb4c4c0eff3"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Chris Cowan <chris@elastic.co>
SoniaSanzV pushed a commit to SoniaSanzV/kibana that referenced this pull request Dec 9, 2024
## 🍒  Summary

This PR closes elastic/streams-program#18 by
adding some basic type checking to the painless output for the Stream
conditions. The new code will check to ensure that none of the fields
used in the condition are `Map` objects. Then it wraps the if statement
in a `try/catch`.

### Condition
```Typescript
{
  and: [
    { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
    {
      or: [
        { field: 'log.level', operator: 'eq' as const, value: 'error' },
        { field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
      ],
    },
  ],
}
```

### Before

```
(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))
```

### After

```
if (ctx.log?.logger instanceof Map || ctx.log?.level instanceof Map) {
  return false;
}
try {
  if ((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))) {
    return true;
  }
  return false;
} catch (Exception e) {
  return false;
}
```
SoniaSanzV pushed a commit to SoniaSanzV/kibana that referenced this pull request Dec 9, 2024
## 🍒  Summary

This PR closes elastic/streams-program#18 by
adding some basic type checking to the painless output for the Stream
conditions. The new code will check to ensure that none of the fields
used in the condition are `Map` objects. Then it wraps the if statement
in a `try/catch`.

### Condition
```Typescript
{
  and: [
    { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
    {
      or: [
        { field: 'log.level', operator: 'eq' as const, value: 'error' },
        { field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
      ],
    },
  ],
}
```

### Before

```
(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))
```

### After

```
if (ctx.log?.logger instanceof Map || ctx.log?.level instanceof Map) {
  return false;
}
try {
  if ((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))) {
    return true;
  }
  return false;
} catch (Exception e) {
  return false;
}
```
CAWilson94 pushed a commit to CAWilson94/kibana that referenced this pull request Dec 9, 2024
## 🍒  Summary

This PR closes elastic/streams-program#18 by
adding some basic type checking to the painless output for the Stream
conditions. The new code will check to ensure that none of the fields
used in the condition are `Map` objects. Then it wraps the if statement
in a `try/catch`.

### Condition
```Typescript
{
  and: [
    { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
    {
      or: [
        { field: 'log.level', operator: 'eq' as const, value: 'error' },
        { field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
      ],
    },
  ],
}
```

### Before

```
(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))
```

### After

```
if (ctx.log?.logger instanceof Map || ctx.log?.level instanceof Map) {
  return false;
}
try {
  if ((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))) {
    return true;
  }
  return false;
} catch (Exception e) {
  return false;
}
```
Samiul-TheSoccerFan pushed a commit to Samiul-TheSoccerFan/kibana that referenced this pull request Dec 10, 2024
## 🍒  Summary

This PR closes elastic/streams-program#18 by
adding some basic type checking to the painless output for the Stream
conditions. The new code will check to ensure that none of the fields
used in the condition are `Map` objects. Then it wraps the if statement
in a `try/catch`.

### Condition
```Typescript
{
  and: [
    { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
    {
      or: [
        { field: 'log.level', operator: 'eq' as const, value: 'error' },
        { field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
      ],
    },
  ],
}
```

### Before

```
(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))
```

### After

```
if (ctx.log?.logger instanceof Map || ctx.log?.level instanceof Map) {
  return false;
}
try {
  if ((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))) {
    return true;
  }
  return false;
} catch (Exception e) {
  return false;
}
```
mykolaharmash pushed a commit to mykolaharmash/kibana that referenced this pull request Dec 11, 2024
## 🍒  Summary

This PR closes elastic/streams-program#18 by
adding some basic type checking to the painless output for the Stream
conditions. The new code will check to ensure that none of the fields
used in the condition are `Map` objects. Then it wraps the if statement
in a `try/catch`.

### Condition
```Typescript
{
  and: [
    { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
    {
      or: [
        { field: 'log.level', operator: 'eq' as const, value: 'error' },
        { field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
      ],
    },
  ],
}
```

### Before

```
(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))
```

### After

```
if (ctx.log?.logger instanceof Map || ctx.log?.level instanceof Map) {
  return false;
}
try {
  if ((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))) {
    return true;
  }
  return false;
} catch (Exception e) {
  return false;
}
```
CAWilson94 pushed a commit to CAWilson94/kibana that referenced this pull request Dec 12, 2024
## 🍒  Summary

This PR closes elastic/streams-program#18 by
adding some basic type checking to the painless output for the Stream
conditions. The new code will check to ensure that none of the fields
used in the condition are `Map` objects. Then it wraps the if statement
in a `try/catch`.

### Condition
```Typescript
{
  and: [
    { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
    {
      or: [
        { field: 'log.level', operator: 'eq' as const, value: 'error' },
        { field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
      ],
    },
  ],
}
```

### Before

```
(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))
```

### After

```
if (ctx.log?.logger instanceof Map || ctx.log?.level instanceof Map) {
  return false;
}
try {
  if ((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))) {
    return true;
  }
  return false;
} catch (Exception e) {
  return false;
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:prev-minor Backport to (8.x) the previous minor version (i.e. one version back from main) Feature:Streams This is the label for the Streams Project release_note:skip Skip the PR/issue when compiling release notes v8.18.0 v9.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants