Skip to content

Commit 6db3071

Browse files
CLOUDP-290417: [Product Metrics/Observability] Implement Collector class
1 parent 2905b28 commit 6db3071

10 files changed

+113
-15
lines changed

tools/spectral/ipa/__tests__/utils/exceptions.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ const objectWithIpa100And101Exception = {
4040
describe('tools/spectral/ipa/rulesets/functions/utils/exceptions.js', () => {
4141
describe('hasException', () => {
4242
it('returns true if object has exception matching the rule name', () => {
43-
expect(hasException(objectWithIpa100Exception, TEST_RULE_NAME_100)).toBe(true);
44-
expect(hasException(objectWithIpa100ExceptionAndOwnerExtension, TEST_RULE_NAME_100)).toBe(true);
45-
expect(hasException(objectWithIpa100And101Exception, TEST_RULE_NAME_100)).toBe(true);
43+
expect(hasException(objectWithIpa100Exception, TEST_RULE_NAME_100, '')).toBe(true);
44+
expect(hasException(objectWithIpa100ExceptionAndOwnerExtension, TEST_RULE_NAME_100, '')).toBe(true);
45+
expect(hasException(objectWithIpa100And101Exception, TEST_RULE_NAME_100, '')).toBe(true);
4646
});
4747
it('returns false if object does not have exception matching the rule name', () => {
48-
expect(hasException({}, TEST_RULE_NAME_100)).toBe(false);
49-
expect(hasException(objectWithIpa101Exception, TEST_RULE_NAME_100)).toBe(false);
48+
expect(hasException({}, TEST_RULE_NAME_100, '')).toBe(false);
49+
expect(hasException(objectWithIpa101Exception, TEST_RULE_NAME_100, '')).toBe(false);
5050
});
5151
it('returns false if object has nested exception matching the rule name', () => {
52-
expect(hasException(objectWithNestedIpa100Exception, TEST_RULE_NAME_100)).toBe(false);
52+
expect(hasException(objectWithNestedIpa100Exception, TEST_RULE_NAME_100, '')).toBe(false);
5353
});
5454
});
5555
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as fs from 'node:fs';
2+
3+
export const EntryType = Object.freeze({
4+
EXCEPTION: 'exceptions',
5+
VIOLATION: 'violations',
6+
ADOPTION: 'adoptions',
7+
});
8+
9+
class Collector {
10+
static instance;
11+
12+
constructor() {
13+
if (Collector.instance) {
14+
return Collector.instance;
15+
}
16+
17+
this.entries = {
18+
[EntryType.VIOLATION]: [],
19+
[EntryType.ADOPTION]: [],
20+
[EntryType.EXCEPTION]: [],
21+
};
22+
this.fileName = "combined.log"
23+
//this.exceptionFileName = 'exceptions.log';
24+
//this.violationFileName = 'violations.log';
25+
//this.adoptionFileName = 'adoptions.log';
26+
27+
28+
process.on('exit', () => this.flushToFile());
29+
process.on('SIGINT', () => {
30+
this.flushToFile();
31+
process.exit(); // Ensure process exits on Ctrl+C
32+
});
33+
34+
Collector.instance = this;
35+
}
36+
37+
add(componentId, ruleName, type) {
38+
if(componentId && ruleName && type) {
39+
componentId = componentId.join('.');
40+
const data = {componentId, ruleName};
41+
this.entries[type].push(data);
42+
}
43+
}
44+
45+
flushToFile() {
46+
try {
47+
const data = JSON.stringify(this.entries, null, 2);
48+
fs.writeFileSync(this.fileName, data);
49+
} catch (error) {
50+
console.error('Error writing exceptions to file:', error);
51+
}
52+
}
53+
}
54+
55+
const collector = new Collector();
56+
export default collector;

tools/spectral/ipa/rulesets/functions/eachCustomMethodMustBeGetOrPost.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isCustomMethod } from './utils/resourceEvaluation.js';
22
import { hasException } from './utils/exceptions.js';
3+
import collector, { EntryType } from '../../metrics/Collector.js';
34

45
const RULE_NAME = 'xgen-IPA-109-custom-method-must-be-GET-or-POST';
56
const ERROR_MESSAGE = 'The HTTP method for custom methods must be GET or POST.';
@@ -13,7 +14,7 @@ export default (input, opts, { path }) => {
1314

1415
if (!isCustomMethod(pathKey)) return;
1516

16-
if (hasException(input, RULE_NAME)) {
17+
if (hasException(input, RULE_NAME, path)) {
1718
return;
1819
}
1920

@@ -23,13 +24,17 @@ export default (input, opts, { path }) => {
2324

2425
// Check for invalid methods
2526
if (httpMethods.some((method) => !VALID_METHODS.includes(method))) {
27+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
2628
return ERROR_RESULT;
2729
}
2830

2931
// Check for multiple valid methods
3032
const validMethodCount = httpMethods.filter((method) => VALID_METHODS.includes(method)).length;
3133

3234
if (validMethodCount > 1) {
35+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
3336
return ERROR_RESULT;
3437
}
38+
39+
collector.add(path, RULE_NAME, EntryType.ADOPTION);
3540
};

tools/spectral/ipa/rulesets/functions/eachCustomMethodMustUseCamelCase.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getCustomMethodName, isCustomMethod } from './utils/resourceEvaluation.js';
22
import { hasException } from './utils/exceptions.js';
33
import { casing } from '@stoplight/spectral-functions';
4+
import collector, { EntryType } from '../../metrics/Collector.js';
45

56
const RULE_NAME = 'xgen-IPA-109-custom-method-must-use-camel-case';
67

@@ -10,16 +11,20 @@ export default (input, opts, { path }) => {
1011

1112
if (!isCustomMethod(pathKey)) return;
1213

13-
if (hasException(input, RULE_NAME)) {
14+
if (hasException(input, RULE_NAME, path)) {
1415
return;
1516
}
1617

1718
let methodName = getCustomMethodName(pathKey);
1819
if (methodName.length === 0 || methodName.trim().length === 0) {
20+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
1921
return [{ message: 'Custom method name cannot be empty or blank.' }];
2022
}
2123

2224
if (casing(methodName, { type: 'camel', disallowDigits: true })) {
25+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
2326
return [{ message: `${methodName} must use camelCase format.` }];
2427
}
28+
29+
collector.add(path, RULE_NAME, EntryType.ADOPTION);
2530
};

tools/spectral/ipa/rulesets/functions/eachEnumValueMustBeUpperSnakeCase.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { hasException } from './utils/exceptions.js';
22
import { resolveObject } from './utils/componentUtils.js';
33
import { casing } from '@stoplight/spectral-functions';
4+
import collector, { EntryType } from '../../metrics/Collector.js';
45

56
const RULE_NAME = 'xgen-IPA-123-enum-values-must-be-upper-snake-case';
67
const ERROR_MESSAGE = 'enum value must be UPPER_SNAKE_CASE.';
@@ -17,7 +18,7 @@ export default (input, _, { path, documentInventory }) => {
1718
const oas = documentInventory.resolved;
1819
const schemaPath = getSchemaPathFromEnumPath(path);
1920
const schemaObject = resolveObject(oas, schemaPath);
20-
if (hasException(schemaObject, RULE_NAME)) {
21+
if (hasException(schemaObject, RULE_NAME, schemaPath)) {
2122
return;
2223
}
2324

@@ -33,5 +34,11 @@ export default (input, _, { path, documentInventory }) => {
3334
}
3435
});
3536

37+
if(errors.length === 0) {
38+
collector.add(path, RULE_NAME, EntryType.ADOPTION);
39+
} else {
40+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
41+
}
42+
3643
return errors;
3744
};

tools/spectral/ipa/rulesets/functions/eachPathAlternatesBetweenResourceNameAndPathParam.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isPathParam } from './utils/componentUtils.js';
22
import { hasException } from './utils/exceptions.js';
3+
import collector, { EntryType } from '../../metrics/Collector.js';
34

45
const RULE_NAME = 'xgen-IPA-102-path-alternate-resource-name-path-param';
56
const ERROR_MESSAGE = 'API paths must alternate between resource name and path params.';
@@ -24,9 +25,9 @@ const validatePathStructure = (elements) => {
2425
});
2526
};
2627

27-
export default (input, _, { documentInventory }) => {
28+
export default (input, _, { path, documentInventory }) => {
2829
const oas = documentInventory.resolved;
29-
if (hasException(oas.paths[input], RULE_NAME)) {
30+
if (hasException(oas.paths[input], RULE_NAME, path)) {
3031
return;
3132
}
3233

@@ -43,6 +44,9 @@ export default (input, _, { documentInventory }) => {
4344
let suffix = suffixWithLeadingSlash.slice(1);
4445
let elements = suffix.split('/');
4546
if (!validatePathStructure(elements)) {
47+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
4648
return ERROR_RESULT;
4749
}
50+
51+
collector.add(path, RULE_NAME, EntryType.ADOPTION);
4852
};

tools/spectral/ipa/rulesets/functions/eachResourceHasGetMethod.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,27 @@ import {
77
getResourcePaths,
88
} from './utils/resourceEvaluation.js';
99
import { hasException } from './utils/exceptions.js';
10+
import collector, { EntryType } from '../../metrics/Collector.js';
1011

1112
const RULE_NAME = 'xgen-IPA-104-resource-has-GET';
1213
const ERROR_MESSAGE = 'APIs must provide a get method for resources.';
1314

14-
export default (input, _, { documentInventory }) => {
15+
export default (input, _, { path, documentInventory }) => {
1516
if (isChild(input) || isCustomMethod(input)) {
1617
return;
1718
}
1819

1920
const oas = documentInventory.resolved;
2021

21-
if (hasException(oas.paths[input], RULE_NAME)) {
22+
if (hasException(oas.paths[input], RULE_NAME, path)) {
2223
return;
2324
}
2425

2526
const resourcePaths = getResourcePaths(input, Object.keys(oas.paths));
2627

2728
if (isSingletonResource(resourcePaths)) {
2829
if (!hasGetMethod(oas.paths[resourcePaths[0]])) {
30+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
2931
return [
3032
{
3133
message: ERROR_MESSAGE,
@@ -34,11 +36,14 @@ export default (input, _, { documentInventory }) => {
3436
}
3537
} else if (isStandardResource(resourcePaths)) {
3638
if (!hasGetMethod(oas.paths[resourcePaths[1]])) {
39+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
3740
return [
3841
{
3942
message: ERROR_MESSAGE,
4043
},
4144
];
4245
}
4346
}
47+
48+
collector.add(path, RULE_NAME, EntryType.ADOPTION);
4449
};

tools/spectral/ipa/rulesets/functions/exceptionExtensionFormat.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import collector, { EntryType } from '../../metrics/Collector.js';
2+
3+
const RULE_NAME = 'xgen-IPA-005-exception-extension-format';
14
const ERROR_MESSAGE = 'IPA exceptions must have a valid rule name and a reason.';
25
const RULE_NAME_PREFIX = 'xgen-IPA-';
36

@@ -16,6 +19,12 @@ export default (input, _, { path }) => {
1619
}
1720
});
1821

22+
if(errors.length === 0) {
23+
collector.add(path, RULE_NAME, EntryType.ADOPTION);
24+
} else {
25+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
26+
}
27+
1928
return errors;
2029
};
2130

tools/spectral/ipa/rulesets/functions/singletonHasNoId.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from './utils/resourceEvaluation.js';
88
import { hasException } from './utils/exceptions.js';
99
import { getAllSuccessfulGetResponseSchemas } from './utils/methodUtils.js';
10+
import collector, { EntryType } from '../../metrics/Collector.js';
1011

1112
const RULE_NAME = 'xgen-IPA-113-singleton-must-not-have-id';
1213
const ERROR_MESSAGE = 'Singleton resources must not have a user-provided or system-generated ID.';
@@ -18,7 +19,7 @@ export default (input, opts, { path, documentInventory }) => {
1819
return;
1920
}
2021

21-
if (hasException(input, RULE_NAME)) {
22+
if (hasException(input, RULE_NAME, path)) {
2223
return;
2324
}
2425

@@ -28,13 +29,17 @@ export default (input, opts, { path, documentInventory }) => {
2829
if (isSingletonResource(resourcePaths) && hasGetMethod(input)) {
2930
const resourceSchemas = getAllSuccessfulGetResponseSchemas(input);
3031
if (resourceSchemas.some((schema) => schemaHasIdProperty(schema))) {
32+
collector.add(path, RULE_NAME, EntryType.VIOLATION);
3133
return [
3234
{
3335
message: ERROR_MESSAGE,
3436
},
3537
];
3638
}
3739
}
40+
41+
collector.add(path, RULE_NAME, EntryType.ADOPTION);
42+
3843
};
3944

4045
function schemaHasIdProperty(schema) {

tools/spectral/ipa/rulesets/functions/utils/exceptions.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import collector, { EntryType } from '../../../metrics/Collector.js';
12
const EXCEPTION_EXTENSION = 'x-xgen-IPA-exception';
23

34
/**
@@ -7,8 +8,9 @@ const EXCEPTION_EXTENSION = 'x-xgen-IPA-exception';
78
* @param ruleName the name of the exempted rule
89
* @returns {boolean} true if the object has an exception named ruleName, otherwise false
910
*/
10-
export function hasException(object, ruleName) {
11+
export function hasException(object, ruleName, path) {
1112
if (object[EXCEPTION_EXTENSION]) {
13+
collector.add(path, ruleName, EntryType.EXCEPTION)
1214
return Object.keys(object[EXCEPTION_EXTENSION]).includes(ruleName);
1315
}
1416
return false;

0 commit comments

Comments
 (0)