Skip to content

Commit

Permalink
🌊 Add type safety to Painless conditions (elastic#202603)
Browse files Browse the repository at this point in the history
## πŸ’  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;
}
```
  • Loading branch information
simianhacker authored and SoniaSanzV committed Dec 9, 2024
1 parent d20862f commit a983938
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 a983938

Please sign in to comment.