Skip to content

Commit c35dc77

Browse files
feat: separate tables and operations (#77)
1 parent 5834c74 commit c35dc77

File tree

5 files changed

+81
-8
lines changed

5 files changed

+81
-8
lines changed

docs/jest-otel/syntax/db-pg.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ So, a complete assertion can look like:
3030
expectTrace(traceloop.serviceByName('orders-service'))
3131
.toQueryPostgreSQL()
3232
.withDatabaseName('postgres')
33-
.withOperationAndTable('INSERT', 'orders')
33+
.withOperations('INSERT')
34+
.withTables('orders')
3435
.withStatement(
3536
/INSERT INTO orders \(id, price_in_cents\) VALUES \('[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}', [0-9]+\)/,
3637
{ compareType: COMPARE_TYPE.REGEX },

package-lock.json

Lines changed: 35 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/expect-opentelemetry/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"@opentelemetry/semantic-conventions": "^1.10.1",
3939
"@traceloop/otel-proto": "^0.6.0",
4040
"axios": "^1.3.4",
41-
"deep-equal": "^2.2.0"
41+
"deep-equal": "^2.2.0",
42+
"node-sql-parser": "^4.7.0"
4243
},
4344
"devDependencies": {
4445
"@types/deep-equal": "^1.0.1",

packages/expect-opentelemetry/src/matchers/service/to-query-postgresql.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ describe('postgresql query', () => {
2424
expectTrace(traceloop.serviceByName('orders-service'))
2525
.toQueryPostgreSQL()
2626
.withDatabaseName('postgres', { compareType: COMPARE_TYPE.EQUALS })
27-
.withOperationAndTable('INSERT', 'orders')
27+
.withOperations('INSERT')
28+
.withTables('orders')
2829
.withStatement(
2930
/INSERT INTO orders \(id, price_in_cents\) VALUES \('[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}', [0-9]+\)/,
3031
{ compareType: COMPARE_TYPE.REGEX },

packages/expect-opentelemetry/src/resources/postgresql-query.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
22
import { opentelemetry } from '@traceloop/otel-proto';
3+
import { Parser } from 'node-sql-parser';
34
import {
45
CompareOptions,
56
filterByAttributeStringValue,
@@ -9,6 +10,7 @@ export class PostgreSQLQuery {
910
constructor(
1011
readonly spans: opentelemetry.proto.trace.v1.ISpan[],
1112
private readonly serviceName: string,
13+
private parser = new Parser(),
1214
) {}
1315

1416
withDatabaseName(name: string | RegExp, options?: CompareOptions) {
@@ -45,9 +47,33 @@ export class PostgreSQLQuery {
4547
return new PostgreSQLQuery(filteredSpans, this.serviceName);
4648
}
4749

48-
withOperationAndTable(operation: string, table: string) {
49-
const regex = new RegExp(`${operation}.*${table}`, 'i'); // case insensitive
50+
withOperations(...operations: string[]) {
51+
const filteredSpans = this.spans.filter((span) => {
52+
const statement = span.attributes?.find(
53+
(attribute) => attribute.key === SemanticAttributes.DB_STATEMENT,
54+
)?.value?.stringValue;
55+
56+
if (!statement) {
57+
return false;
58+
}
59+
60+
const lowerCaseStatement = statement.toLowerCase();
61+
62+
return operations.every((operation) =>
63+
lowerCaseStatement.includes(operation.toLowerCase()),
64+
);
65+
});
5066

67+
if (filteredSpans.length === 0) {
68+
throw new Error(
69+
`No query by ${this.serviceName} to postgresql with operations ${operations} was found`,
70+
);
71+
}
72+
73+
return new PostgreSQLQuery(filteredSpans, this.serviceName);
74+
}
75+
76+
withTables(...tables: string[]) {
5177
const filteredSpans = this.spans.filter((span) => {
5278
const statement = span.attributes?.find(
5379
(attribute) => attribute.key === SemanticAttributes.DB_STATEMENT,
@@ -57,15 +83,25 @@ export class PostgreSQLQuery {
5783
return false;
5884
}
5985

60-
return regex.test(statement);
86+
const allTablesInStatement = this.parser
87+
.tableList(prepareQuery(statement), { database: 'PostgresQL' })
88+
.map((table) => table.split('::')[2].trim());
89+
90+
return tables.every((table) =>
91+
allTablesInStatement.includes(table.toLowerCase()),
92+
);
6193
});
6294

6395
if (filteredSpans.length === 0) {
6496
throw new Error(
65-
`No query by ${this.serviceName} to postgresql with operation ${operation} and table ${table} was found`,
97+
`No query by ${this.serviceName} to postgresql with tables ${tables} was found`,
6698
);
6799
}
68100

69101
return new PostgreSQLQuery(filteredSpans, this.serviceName);
70102
}
71103
}
104+
105+
const prepareQuery = (
106+
query: string, // remove double quotes and replace %s with 111
107+
) => query.replace(/"/g, '').replace(/%s/g, '111').toLocaleLowerCase();

0 commit comments

Comments
 (0)