Skip to content

Commit

Permalink
[8.x] 🌊 Add type safety to Painless conditions (elastic#202603) (elas…
Browse files Browse the repository at this point in the history
…tic#203104)

# Backport

This will backport the following commits from `main` to `8.x`:
- [🌊 Add type safety to Painless conditions
(elastic#202603)](elastic#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":"[email protected]"},"sourceCommit":{"committedDate":"2024-12-05T13:58:36Z","message":"🌊
Add type safety to Painless conditions (elastic#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 (elastic#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 (elastic#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 <[email protected]>
  • Loading branch information
kibanamachine and simianhacker authored Dec 5, 2024
1 parent 92950c1 commit f6f28fd
Show file tree
Hide file tree
Showing 3 changed files with 376 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,53 @@
* 2.0.
*/

import { conditionToPainless } from './condition_to_painless';
import { conditionToPainless, conditionToStatement } from './condition_to_painless';

const operatorConditionAndResults = [
{
condition: { field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
result: '(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy")',
result:
'(ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString() == "nginx_proxy") || ctx.log?.logger == "nginx_proxy"))',
},
{
condition: { field: 'log.logger', operator: 'neq' as const, value: 'nginx_proxy' },
result: '(ctx.log?.logger !== null && ctx.log?.logger != "nginx_proxy")',
result:
'(ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString() != "nginx_proxy") || ctx.log?.logger != "nginx_proxy"))',
},
{
condition: { field: 'http.response.status_code', operator: 'lt' as const, value: 500 },
result: '(ctx.http?.response?.status_code !== null && ctx.http?.response?.status_code < 500)',
result:
'(ctx.http?.response?.status_code !== null && ((ctx.http?.response?.status_code instanceof String && Float.parseFloat(ctx.http?.response?.status_code) < 500) || ctx.http?.response?.status_code < 500))',
},
{
condition: { field: 'http.response.status_code', operator: 'lte' as const, value: 500 },
result: '(ctx.http?.response?.status_code !== null && ctx.http?.response?.status_code <= 500)',
result:
'(ctx.http?.response?.status_code !== null && ((ctx.http?.response?.status_code instanceof String && Float.parseFloat(ctx.http?.response?.status_code) <= 500) || ctx.http?.response?.status_code <= 500))',
},
{
condition: { field: 'http.response.status_code', operator: 'gt' as const, value: 500 },
result: '(ctx.http?.response?.status_code !== null && ctx.http?.response?.status_code > 500)',
result:
'(ctx.http?.response?.status_code !== null && ((ctx.http?.response?.status_code instanceof String && Float.parseFloat(ctx.http?.response?.status_code) > 500) || ctx.http?.response?.status_code > 500))',
},
{
condition: { field: 'http.response.status_code', operator: 'gte' as const, value: 500 },
result: '(ctx.http?.response?.status_code !== null && ctx.http?.response?.status_code >= 500)',
result:
'(ctx.http?.response?.status_code !== null && ((ctx.http?.response?.status_code instanceof String && Float.parseFloat(ctx.http?.response?.status_code) >= 500) || ctx.http?.response?.status_code >= 500))',
},
{
condition: { field: 'log.logger', operator: 'startsWith' as const, value: 'nginx' },
result: '(ctx.log?.logger !== null && ctx.log?.logger.startsWith("nginx"))',
result:
'(ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString().startsWith("nginx")) || ctx.log?.logger.startsWith("nginx")))',
},
{
condition: { field: 'log.logger', operator: 'endsWith' as const, value: 'proxy' },
result: '(ctx.log?.logger !== null && ctx.log?.logger.endsWith("proxy"))',
result:
'(ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString().endsWith("proxy")) || ctx.log?.logger.endsWith("proxy")))',
},
{
condition: { field: 'log.logger', operator: 'contains' as const, value: 'proxy' },
result: '(ctx.log?.logger !== null && ctx.log?.logger.contains("proxy"))',
result:
'(ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString().contains("proxy")) || ctx.log?.logger.contains("proxy")))',
},
{
condition: { field: 'log.logger', operator: 'exists' as const },
Expand All @@ -55,87 +64,152 @@ const operatorConditionAndResults = [
];

describe('conditionToPainless', () => {
describe('operators', () => {
operatorConditionAndResults.forEach((setup) => {
test(`${setup.condition.operator}`, () => {
expect(conditionToPainless(setup.condition)).toEqual(setup.result);
describe('conditionToStatement', () => {
describe('operators', () => {
operatorConditionAndResults.forEach((setup) => {
test(`${setup.condition.operator}`, () => {
expect(conditionToStatement(setup.condition)).toEqual(setup.result);
});
});
});
});

describe('and', () => {
test('simple', () => {
const condition = {
and: [
{ field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
{ field: 'log.level', operator: 'eq' as const, value: 'error' },
],
};
expect(
expect(conditionToPainless(condition)).toEqual(
'(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") && (ctx.log?.level !== null && ctx.log?.level == "error")'
)
);
test('ensure number comparasion works with string values', () => {
const condition = {
field: 'http.response.status_code',
operator: 'gt' as const,
value: '500',
};
expect(conditionToStatement(condition)).toEqual(
'(ctx.http?.response?.status_code !== null && ((ctx.http?.response?.status_code instanceof String && Float.parseFloat(ctx.http?.response?.status_code) > 500) || ctx.http?.response?.status_code > 500))'
);
});
test('ensure string comparasion works with number values', () => {
const condition = {
field: 'message',
operator: 'contains' as const,
value: 500,
};
expect(conditionToStatement(condition)).toEqual(
'(ctx.message !== null && ((ctx.message instanceof Number && ctx.message.toString().contains("500")) || ctx.message.contains("500")))'
);
});
});
});

describe('or', () => {
test('simple', () => {
const condition = {
or: [
{ field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
{ field: 'log.level', operator: 'eq' as const, value: 'error' },
],
};
expect(
expect(conditionToPainless(condition)).toEqual(
'(ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") || (ctx.log?.level !== null && ctx.log?.level == "error")'
)
);
describe('and', () => {
test('simple', () => {
const condition = {
and: [
{ field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
{ field: 'log.level', operator: 'eq' as const, value: 'error' },
],
};
expect(
expect(conditionToStatement(condition)).toEqual(
'(ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString() == "nginx_proxy") || ctx.log?.logger == "nginx_proxy")) && (ctx.log?.level !== null && ((ctx.log?.level instanceof Number && ctx.log?.level.toString() == "error") || ctx.log?.level == "error"))'
)
);
});
});
});

describe('nested', () => {
test('and with a filter and or with 2 filters', () => {
const 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' },
],
},
],
};
expect(
expect(conditionToPainless(condition)).toEqual(
'(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"))'
)
);
describe('or', () => {
test('simple', () => {
const condition = {
or: [
{ field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
{ field: 'log.level', operator: 'eq' as const, value: 'error' },
],
};
expect(
expect(conditionToStatement(condition)).toEqual(
'(ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString() == "nginx_proxy") || ctx.log?.logger == "nginx_proxy")) || (ctx.log?.level !== null && ((ctx.log?.level instanceof Number && ctx.log?.level.toString() == "error") || ctx.log?.level == "error"))'
)
);
});
});
test('and with 2 or with filters', () => {
const condition = {
and: [
{
or: [
{ field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
{ field: 'service.name', operator: 'eq' as const, value: 'nginx' },
],
},
{
or: [
{ field: 'log.level', operator: 'eq' as const, value: 'error' },
{ field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
],
},
],
};
expect(
expect(conditionToPainless(condition)).toEqual(
'((ctx.log?.logger !== null && ctx.log?.logger == "nginx_proxy") || (ctx.service?.name !== null && ctx.service?.name == "nginx")) && ((ctx.log?.level !== null && ctx.log?.level == "error") || (ctx.log?.level !== null && ctx.log?.level == "ERROR"))'
)
);

describe('nested', () => {
test('and with a filter and or with 2 filters', () => {
const 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' },
],
},
],
};
expect(
expect(conditionToStatement(condition)).toEqual(
'(ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString() == "nginx_proxy") || ctx.log?.logger == "nginx_proxy")) && ((ctx.log?.level !== null && ((ctx.log?.level instanceof Number && ctx.log?.level.toString() == "error") || ctx.log?.level == "error")) || (ctx.log?.level !== null && ((ctx.log?.level instanceof Number && ctx.log?.level.toString() == "ERROR") || ctx.log?.level == "ERROR")))'
)
);
});
test('and with 2 or with filters', () => {
const condition = {
and: [
{
or: [
{ field: 'log.logger', operator: 'eq' as const, value: 'nginx_proxy' },
{ field: 'service.name', operator: 'eq' as const, value: 'nginx' },
],
},
{
or: [
{ field: 'log.level', operator: 'eq' as const, value: 'error' },
{ field: 'log.level', operator: 'eq' as const, value: 'ERROR' },
],
},
],
};
expect(
expect(conditionToStatement(condition)).toEqual(
'((ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString() == "nginx_proxy") || ctx.log?.logger == "nginx_proxy")) || (ctx.service?.name !== null && ((ctx.service?.name instanceof Number && ctx.service?.name.toString() == "nginx") || ctx.service?.name == "nginx"))) && ((ctx.log?.level !== null && ((ctx.log?.level instanceof Number && ctx.log?.level.toString() == "error") || ctx.log?.level == "error")) || (ctx.log?.level !== null && ((ctx.log?.level instanceof Number && ctx.log?.level.toString() == "ERROR") || ctx.log?.level == "ERROR")))'
)
);
});
});
});

test('wrapped with type checks for uinary conditions', () => {
const condition = { field: 'log', operator: 'exists' as const };
expect(conditionToPainless(condition)).toEqual(`try {
if (ctx.log !== null) {
return true;
}
return false;
} catch (Exception e) {
return false;
}
`);
});

test('wrapped with typechecks and try/catch', () => {
const 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' },
],
},
],
};
expect(
expect(conditionToPainless(condition))
.toEqual(`if (ctx.log?.logger instanceof Map || ctx.log?.level instanceof Map) {
return false;
}
try {
if ((ctx.log?.logger !== null && ((ctx.log?.logger instanceof Number && ctx.log?.logger.toString() == "nginx_proxy") || ctx.log?.logger == "nginx_proxy")) && ((ctx.log?.level !== null && ((ctx.log?.level instanceof Number && ctx.log?.level.toString() == "error") || ctx.log?.level == "error")) || (ctx.log?.level !== null && ((ctx.log?.level instanceof Number && ctx.log?.level.toString() == "ERROR") || ctx.log?.level == "ERROR")))) {
return true;
}
return false;
} catch (Exception e) {
return false;
}
`)
);
});
});
Loading

0 comments on commit f6f28fd

Please sign in to comment.