cat-a-logs 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +13 -0
- package/README.md +57 -0
- package/bin/catalogExe.js +4 -0
- package/client/App.js +1 -0
- package/client/App.tsx +0 -0
- package/client/css/input.css +3 -0
- package/client/index.js +261 -0
- package/client/index.ts +291 -0
- package/client/template.html +18 -0
- package/jest.config.mjs +26 -0
- package/lambda-nodejs22.x/README.TOOLKIT.md +38 -0
- package/lambda-nodejs22.x/README.md +127 -0
- package/lambda-nodejs22.x/events/event.json +62 -0
- package/lambda-nodejs22.x/hello-world/app.mjs +82 -0
- package/lambda-nodejs22.x/hello-world/package.json +25 -0
- package/lambda-nodejs22.x/samconfig.toml +31 -0
- package/lambda-nodejs22.x/template.yaml +41 -0
- package/package.json +69 -0
- package/postcss.config.js +7 -0
- package/server/server.ts +4 -0
- package/tailwind.config.js +12 -0
- package/tsconfig.json +113 -0
- package/webpack.config.js +79 -0
package/.babelrc
ADDED
package/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# cat-a-log
|
2
|
+
This function will help you create AWS Embedded Metric Format Logs and publish them to AWS Cloudwatch. EMF formatting will allow for chosen metrics to be automatically visualized in graphs for simplier debugging.
|
3
|
+
|
4
|
+
**What is Embedded Metric Formatting (EMF):**\
|
5
|
+
This is a JSON specification to communicate with Cloudwatch Logs to automatically extract values embedded in the structured log events.
|
6
|
+
EMF is especially great for applications that make logs and need custom metrics without more complexity or cost.
|
7
|
+
For more information please visit the link below
|
8
|
+
|
9
|
+
<a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html" target="_blank">AWS Documentation on EMF Formatting</a>
|
10
|
+
|
11
|
+
|
12
|
+
**Why use cat-a-log?:**\
|
13
|
+
Why use a washing machine when you can do them by hand? Becuase it makes your job way easier! Leveraging AWS Lambda Powertools we can use the cat-a-log function to invoke and format logs into AWS Embedded Metric Format. By publishing these logs to AWS Cloudwatch, we are able to provide engineers with automatic metric visulaization to make the process of debugging logs much more efficient. Cat-a-log utilizies a cache to make effcient work of sending logs to Cloudwatch.
|
14
|
+
|
15
|
+
|
16
|
+
**How do I use cat-a-log?:**\
|
17
|
+
npm install our package using the command `npm install cat-a-logs` into your app.mjs file then capture the function and cache (we will talk about this later)
|
18
|
+
`import {cache, catalog} from "cat-a-logs/index.js";`
|
19
|
+
|
20
|
+
The catalog function below takes in the following arguments - lets look at each argument one at a time:
|
21
|
+
|
22
|
+
`function catalog(
|
23
|
+
trackedVariable: number | Array<number>,
|
24
|
+
metricName: string,
|
25
|
+
metricNamespace: string,
|
26
|
+
metricUnitLabel: string = "None",
|
27
|
+
CustomerDefinedDimension: { [key: string]: string } = {},
|
28
|
+
resolution: 1 | 60 = 60,
|
29
|
+
deploy: boolean = false
|
30
|
+
)`
|
31
|
+
|
32
|
+
- **trackedVariable**: This is a number that is dynamic and can change with each call - these numbers are reflected
|
33
|
+
- **metricName**:
|
34
|
+
- **metricNamespace**:
|
35
|
+
- **metricUnitLabel**: Can only be the following labels:
|
36
|
+
- Seconds | Microseconds | Milliseconds | Bytes | Kilobytes | Megabytes | Gigabytes | Terabytes | Bits | Kilobits | Megabits | Gigabits | Terabits | Percent | Count | Bytes/Second | Kilobytes/Second | Megabytes/Second | Gigabytes/Second | Terabytes/Second | Bits/Second | Kilobits/Second | Megabits/Second | Gigabits/Second | Terabits/Second | Count/Second | None\
|
37
|
+
|
38
|
+
To read more about Metric Datum see this <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html" target="_blank">link</a>
|
39
|
+
- **CustomerDefinedDimension**:
|
40
|
+
- **resolution**:
|
41
|
+
- **deploy**:
|
42
|
+
|
43
|
+
**Structure of the files:**\
|
44
|
+
<a href="https://www.npmjs.com/package/cat-a-logs?activeTab=readme" target="_blank">Link to npm package</a>
|
45
|
+
|
46
|
+
`index.ts` is compiled to `index.js`. Important to compile `.ts` file to es6 js syntax using the `tsc —target es6 (filepath)` command
|
47
|
+
`app.mjs ` is a "pathway" to our lambda function. Here is where we will import catalog function and use it to involke our lambda function
|
48
|
+
|
49
|
+
index.ts lines 25-30 is checking to see if the value "level" || "message" || "sampling_rate" || "service" || "timestamp" ||"xray_trace_id"
|
50
|
+
|
51
|
+
logger.info gives you some information level is key and value is info
|
52
|
+
|
53
|
+
|
54
|
+
if you write name that it will overwrite the keys
|
55
|
+
|
56
|
+
**Tech Challenges**
|
57
|
+
Spent 3 days dealing with inconsistencies of ES6/CommonJS in our code before compiling .js in ES6
|
package/client/App.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
"use strict";
|
package/client/App.tsx
ADDED
File without changes
|
package/client/index.js
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
8
|
+
});
|
9
|
+
};
|
10
|
+
import { Logger } from "@aws-lambda-powertools/logger";
|
11
|
+
import { Ajv } from "ajv";
|
12
|
+
//cache entries are structured thusly: 'Namespace + Dimensions(Alphabetically)': EMFObject
|
13
|
+
const cache = {};
|
14
|
+
//catalog(kilos, "kilos" , "lambda-function-metrics", "Kilograms", {'functionVersion': $LATEST, 'testDimension': derp});
|
15
|
+
function catalog(trackedVariable_1, metricName_1, metricNamespace_1) {
|
16
|
+
return __awaiter(this, arguments, void 0, function* (trackedVariable, metricName, metricNamespace, metricUnitLabel = "None", CustomerDefinedDimension = {}, resolution = 60, deploy = false) {
|
17
|
+
//Check for any errors & validate inputs based on documentations
|
18
|
+
if (!cache)
|
19
|
+
throw new Error("cache is not found, please import cache from cat-a-log");
|
20
|
+
console.log(Object.keys(CustomerDefinedDimension).concat([metricName.toLowerCase()]));
|
21
|
+
const badKeys = ["level", "message", "sampling_rate", "service", "timestamp", "xray_trace_id"];
|
22
|
+
const yourKeys = Object.keys(CustomerDefinedDimension).concat([metricName.toLowerCase()]);
|
23
|
+
for (let i = 0; i < yourKeys.length; i++) {
|
24
|
+
if (badKeys.includes(yourKeys[i])) {
|
25
|
+
throw new Error("metricName, or Dimension names cannot be the same as these native logger keys: level || message || sampling_rate || service || timestamp || xray_trace_id");
|
26
|
+
}
|
27
|
+
}
|
28
|
+
if (Array.isArray(trackedVariable)) {
|
29
|
+
if (trackedVariable.length > 100)
|
30
|
+
throw new Error("metric value cannot have more than 100 elements");
|
31
|
+
}
|
32
|
+
if (Object.keys(CustomerDefinedDimension).length > 30) {
|
33
|
+
throw new Error("EMF has a limit of 30 user defined dimension keys per log");
|
34
|
+
}
|
35
|
+
const logger = new Logger({ serviceName: "serverlessAirline" });
|
36
|
+
// Ajv instance
|
37
|
+
const ajv = new Ajv();
|
38
|
+
// from AWS: EMF schema to test/validate against
|
39
|
+
const emfSchema = {
|
40
|
+
type: "object",
|
41
|
+
title: "Root Node",
|
42
|
+
required: ["_aws"],
|
43
|
+
properties: {
|
44
|
+
_aws: {
|
45
|
+
$id: "#/properties/_aws",
|
46
|
+
type: "object",
|
47
|
+
title: "Metadata",
|
48
|
+
required: ["Timestamp", "CloudWatchMetrics"],
|
49
|
+
properties: {
|
50
|
+
Timestamp: {
|
51
|
+
$id: "#/properties/_aws/properties/Timestamp",
|
52
|
+
type: "integer",
|
53
|
+
title: "The Timestamp Schema",
|
54
|
+
examples: [1565375354953],
|
55
|
+
},
|
56
|
+
CloudWatchMetrics: {
|
57
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics",
|
58
|
+
type: "array",
|
59
|
+
title: "MetricDirectives",
|
60
|
+
items: {
|
61
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items",
|
62
|
+
type: "object",
|
63
|
+
title: "MetricDirective",
|
64
|
+
required: ["Namespace", "Dimensions", "Metrics"],
|
65
|
+
properties: {
|
66
|
+
Namespace: {
|
67
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Namespace",
|
68
|
+
type: "string",
|
69
|
+
title: "CloudWatch Metrics Namespace",
|
70
|
+
examples: ["MyApp"],
|
71
|
+
pattern: "^(.*)$",
|
72
|
+
minLength: 1,
|
73
|
+
maxLength: 1024,
|
74
|
+
},
|
75
|
+
Dimensions: {
|
76
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions",
|
77
|
+
type: "array",
|
78
|
+
title: "The Dimensions Schema",
|
79
|
+
minItems: 1,
|
80
|
+
items: {
|
81
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items",
|
82
|
+
type: "array",
|
83
|
+
title: "DimensionSet",
|
84
|
+
minItems: 0,
|
85
|
+
maxItems: 30,
|
86
|
+
items: {
|
87
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items/items",
|
88
|
+
type: "string",
|
89
|
+
title: "DimensionReference",
|
90
|
+
examples: ["Operation"],
|
91
|
+
pattern: "^(.*)$",
|
92
|
+
minLength: 1,
|
93
|
+
maxLength: 250,
|
94
|
+
},
|
95
|
+
},
|
96
|
+
},
|
97
|
+
Metrics: {
|
98
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics",
|
99
|
+
type: "array",
|
100
|
+
title: "MetricDefinitions",
|
101
|
+
items: {
|
102
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items",
|
103
|
+
type: "object",
|
104
|
+
title: "MetricDefinition",
|
105
|
+
required: ["Name"],
|
106
|
+
properties: {
|
107
|
+
Name: {
|
108
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Name",
|
109
|
+
type: "string",
|
110
|
+
title: "MetricName",
|
111
|
+
examples: ["ProcessingLatency"],
|
112
|
+
pattern: "^(.*)$",
|
113
|
+
minLength: 1,
|
114
|
+
maxLength: 1024,
|
115
|
+
},
|
116
|
+
Unit: {
|
117
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Unit",
|
118
|
+
type: "string",
|
119
|
+
title: "MetricUnit",
|
120
|
+
examples: ["Milliseconds"],
|
121
|
+
pattern: "^(Seconds|Microseconds|Milliseconds|Bytes|Kilobytes|Megabytes|Gigabytes|Terabytes|Bits|Kilobits|Megabits|Gigabits|Terabits|Percent|Count|Bytes\\/Second|Kilobytes\\/Second|Megabytes\\/Second|Gigabytes\\/Second|Terabytes\\/Second|Bits\\/Second|Kilobits\\/Second|Megabits\\/Second|Gigabits\\/Second|Terabits\\/Second|Count\\/Second|None)$",
|
122
|
+
},
|
123
|
+
StorageResolution: {
|
124
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/StorageResolution",
|
125
|
+
type: "integer",
|
126
|
+
title: "StorageResolution",
|
127
|
+
examples: [60],
|
128
|
+
},
|
129
|
+
},
|
130
|
+
},
|
131
|
+
},
|
132
|
+
},
|
133
|
+
},
|
134
|
+
},
|
135
|
+
},
|
136
|
+
},
|
137
|
+
},
|
138
|
+
};
|
139
|
+
const validateEmf = ajv.compile(emfSchema);
|
140
|
+
//sort customerDimensions key values in alphabetical order
|
141
|
+
const sortedDimensions = {};
|
142
|
+
for (let i = 0; i < Object.keys(CustomerDefinedDimension).sort().length; i++) {
|
143
|
+
sortedDimensions[Object.keys(CustomerDefinedDimension).sort()[i]] =
|
144
|
+
CustomerDefinedDimension[Object.keys(CustomerDefinedDimension).sort()[i]];
|
145
|
+
}
|
146
|
+
//if Object with Namespace and Dimensions already exists in Set
|
147
|
+
let check = cache[`${metricNamespace}${sortedDimensions}`];
|
148
|
+
if (check != undefined) {
|
149
|
+
//push the metrics object to Metrics array
|
150
|
+
cache[`${metricNamespace}${sortedDimensions}`]["_aws"]["CloudWatchMetrics"][0]["Metrics"].push({
|
151
|
+
Name: metricName,
|
152
|
+
Unit: metricUnitLabel,
|
153
|
+
StorageResolution: resolution,
|
154
|
+
});
|
155
|
+
//add key value to Log
|
156
|
+
check[`${metricName}`] = trackedVariable;
|
157
|
+
}
|
158
|
+
else {
|
159
|
+
// //create new Structured Log and add it to cachedStructuredLogs - BMA 1/18/25 removed to test Ajv
|
160
|
+
// cache[`${metricNamespace}${sortedDimensions}`] = Object.assign(
|
161
|
+
// NameSpace & Dimensions for EMF part don't exist yet. Initialize variable to capture EMF/aws key:value pair
|
162
|
+
const newEmfLog = Object.assign({
|
163
|
+
_aws: {
|
164
|
+
Timestamp: Date.now(),
|
165
|
+
CloudWatchMetrics: [
|
166
|
+
{
|
167
|
+
Namespace: metricNamespace,
|
168
|
+
Dimensions: [Object.keys(sortedDimensions)],
|
169
|
+
Metrics: [
|
170
|
+
{
|
171
|
+
Name: metricName,
|
172
|
+
Unit: metricUnitLabel,
|
173
|
+
StorageResolution: resolution,
|
174
|
+
},
|
175
|
+
],
|
176
|
+
},
|
177
|
+
],
|
178
|
+
},
|
179
|
+
[`${metricName}`]: trackedVariable,
|
180
|
+
}, CustomerDefinedDimension);
|
181
|
+
// Log the Unit value before validation
|
182
|
+
console.log("index.ts - Unit value before validation: ", newEmfLog._aws.CloudWatchMetrics[0].Metrics[0].Unit);
|
183
|
+
// validate the new EMF JSON schema against AWS EMF JSON schema before adding to cache object
|
184
|
+
const isValid = validateEmf(newEmfLog);
|
185
|
+
// //troubleshooting console.error in test
|
186
|
+
// console.log('index.ts - Validation result: ', isValid);
|
187
|
+
// troubleshooting console.error in emf test
|
188
|
+
// console.log('index.ts - Validation errors: ', validateEmf.errors);
|
189
|
+
// if it fails validation throw error
|
190
|
+
if (!isValid) {
|
191
|
+
console.error("An error occurred during EMF validation: ", validateEmf.errors);
|
192
|
+
throw new Error("Supplied/Proposed structured log does not comply with EMF schema");
|
193
|
+
}
|
194
|
+
// If it passes then add to cache object
|
195
|
+
cache[`${metricNamespace}${sortedDimensions}`] = newEmfLog;
|
196
|
+
}
|
197
|
+
if (deploy) {
|
198
|
+
//after last catalog function is invoked, send all cached logs with logger at once
|
199
|
+
for (let i = 0; i < Object.keys(cache).length; i++) {
|
200
|
+
logger.info(`Your EMF compliant Structured Metrics Log ${i + 1}`, cache[Object.keys(cache)[i]]);
|
201
|
+
}
|
202
|
+
//clear cache
|
203
|
+
console.log("BEFORE:", cache);
|
204
|
+
for (var member in cache)
|
205
|
+
delete cache[member];
|
206
|
+
console.log("AFTER:", cache);
|
207
|
+
}
|
208
|
+
});
|
209
|
+
}
|
210
|
+
export { cache, catalog };
|
211
|
+
/*Current Working logger invocation
|
212
|
+
logger.info("Your EMF compliant Structured Metrics Log",
|
213
|
+
Object.assign({
|
214
|
+
_aws: {
|
215
|
+
Timestamp: Date.now(),
|
216
|
+
CloudWatchMetrics: [
|
217
|
+
{
|
218
|
+
Namespace: metricNamespace,
|
219
|
+
Dimensions: [Object.keys(CustomerDefinedDimension)],
|
220
|
+
Metrics: [
|
221
|
+
{
|
222
|
+
Name: metricName,
|
223
|
+
Unit: metricUnitLabel,
|
224
|
+
StorageResolution: resolution,
|
225
|
+
}
|
226
|
+
]
|
227
|
+
}
|
228
|
+
]
|
229
|
+
},
|
230
|
+
[`${metricName}`]: trackedVariable,
|
231
|
+
},
|
232
|
+
CustomerDefinedDimension
|
233
|
+
)
|
234
|
+
|
235
|
+
)
|
236
|
+
*/
|
237
|
+
//Old handler function
|
238
|
+
// export const handler = async (_event, _context): Promise<void> => {
|
239
|
+
// const testObj = {
|
240
|
+
// testguy: "hi",
|
241
|
+
// fool: 42,
|
242
|
+
// functionVersion: "$LATEST",
|
243
|
+
// _aws: {
|
244
|
+
// Timestamp: Date.now(),
|
245
|
+
// CloudWatchMetrics: [
|
246
|
+
// {
|
247
|
+
// Namespace: "lambda-function-metrics",
|
248
|
+
// Dimensions: [["functionVersion"]],
|
249
|
+
// Metrics: [
|
250
|
+
// {
|
251
|
+
// Name: "fool",
|
252
|
+
// Unit: "Milliseconds",
|
253
|
+
// StorageResolution: 60
|
254
|
+
// }
|
255
|
+
// ]
|
256
|
+
// }
|
257
|
+
// ]
|
258
|
+
// },
|
259
|
+
// }
|
260
|
+
// logger.info(JSON.stringify(testObj));
|
261
|
+
// };
|
package/client/index.ts
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
import { Logger } from "@aws-lambda-powertools/logger";
|
2
|
+
import { Ajv } from "ajv";
|
3
|
+
|
4
|
+
//cache entries are structured thusly: 'Namespace + Dimensions(Alphabetically)': EMFObject
|
5
|
+
const cache: { [key: string]: any } = {};
|
6
|
+
//catalog(kilos, "kilos" , "lambda-function-metrics", "Kilograms", {'functionVersion': $LATEST, 'testDimension': derp});
|
7
|
+
async function catalog(
|
8
|
+
trackedVariable: number | Array<number>,
|
9
|
+
metricName: string,
|
10
|
+
metricNamespace: string,
|
11
|
+
metricUnitLabel: string = "None",
|
12
|
+
CustomerDefinedDimension: { [key: string]: string } = {},
|
13
|
+
resolution: 1 | 60 = 60,
|
14
|
+
deploy: boolean = false
|
15
|
+
): Promise<void> {
|
16
|
+
//Check for any errors & validate inputs based on documentations
|
17
|
+
if (!cache)
|
18
|
+
throw new Error("cache is not found, please import cache from cat-a-log");
|
19
|
+
console.log(Object.keys(CustomerDefinedDimension).concat([metricName.toLowerCase()]));
|
20
|
+
const badKeys = ["level", "message", "sampling_rate", "service", "timestamp", "xray_trace_id"];
|
21
|
+
const yourKeys = Object.keys(CustomerDefinedDimension).concat([metricName.toLowerCase()]);
|
22
|
+
for(let i = 0; i < yourKeys.length; i++){
|
23
|
+
if(badKeys.includes(yourKeys[i])){
|
24
|
+
throw new Error(
|
25
|
+
"metricName, or Dimension names cannot be the same as these native logger keys: level || message || sampling_rate || service || timestamp || xray_trace_id"
|
26
|
+
);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
if (Array.isArray(trackedVariable)) {
|
33
|
+
if (trackedVariable.length > 100)
|
34
|
+
throw new Error("metric value cannot have more than 100 elements");
|
35
|
+
}
|
36
|
+
if (Object.keys(CustomerDefinedDimension).length > 30) {
|
37
|
+
throw new Error(
|
38
|
+
"EMF has a limit of 30 user defined dimension keys per log"
|
39
|
+
);
|
40
|
+
}
|
41
|
+
const logger = new Logger({ serviceName: "serverlessAirline" });
|
42
|
+
// Ajv instance
|
43
|
+
const ajv = new Ajv();
|
44
|
+
// from AWS: EMF schema to test/validate against
|
45
|
+
const emfSchema = {
|
46
|
+
type: "object",
|
47
|
+
title: "Root Node",
|
48
|
+
required: ["_aws"],
|
49
|
+
properties: {
|
50
|
+
_aws: {
|
51
|
+
$id: "#/properties/_aws",
|
52
|
+
type: "object",
|
53
|
+
title: "Metadata",
|
54
|
+
required: ["Timestamp", "CloudWatchMetrics"],
|
55
|
+
properties: {
|
56
|
+
Timestamp: {
|
57
|
+
$id: "#/properties/_aws/properties/Timestamp",
|
58
|
+
type: "integer",
|
59
|
+
title: "The Timestamp Schema",
|
60
|
+
examples: [1565375354953],
|
61
|
+
},
|
62
|
+
CloudWatchMetrics: {
|
63
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics",
|
64
|
+
type: "array",
|
65
|
+
title: "MetricDirectives",
|
66
|
+
items: {
|
67
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items",
|
68
|
+
type: "object",
|
69
|
+
title: "MetricDirective",
|
70
|
+
required: ["Namespace", "Dimensions", "Metrics"],
|
71
|
+
properties: {
|
72
|
+
Namespace: {
|
73
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Namespace",
|
74
|
+
type: "string",
|
75
|
+
title: "CloudWatch Metrics Namespace",
|
76
|
+
examples: ["MyApp"],
|
77
|
+
pattern: "^(.*)$",
|
78
|
+
minLength: 1,
|
79
|
+
maxLength: 1024,
|
80
|
+
},
|
81
|
+
Dimensions: {
|
82
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions",
|
83
|
+
type: "array",
|
84
|
+
title: "The Dimensions Schema",
|
85
|
+
minItems: 1,
|
86
|
+
items: {
|
87
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items",
|
88
|
+
type: "array",
|
89
|
+
title: "DimensionSet",
|
90
|
+
minItems: 0,
|
91
|
+
maxItems: 30,
|
92
|
+
items: {
|
93
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items/items",
|
94
|
+
type: "string",
|
95
|
+
title: "DimensionReference",
|
96
|
+
examples: ["Operation"],
|
97
|
+
pattern: "^(.*)$",
|
98
|
+
minLength: 1,
|
99
|
+
maxLength: 250,
|
100
|
+
},
|
101
|
+
},
|
102
|
+
},
|
103
|
+
Metrics: {
|
104
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics",
|
105
|
+
type: "array",
|
106
|
+
title: "MetricDefinitions",
|
107
|
+
items: {
|
108
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items",
|
109
|
+
type: "object",
|
110
|
+
title: "MetricDefinition",
|
111
|
+
required: ["Name"],
|
112
|
+
properties: {
|
113
|
+
Name: {
|
114
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Name",
|
115
|
+
type: "string",
|
116
|
+
title: "MetricName",
|
117
|
+
examples: ["ProcessingLatency"],
|
118
|
+
pattern: "^(.*)$",
|
119
|
+
minLength: 1,
|
120
|
+
maxLength: 1024,
|
121
|
+
},
|
122
|
+
Unit: {
|
123
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Unit",
|
124
|
+
type: "string",
|
125
|
+
title: "MetricUnit",
|
126
|
+
examples: ["Milliseconds"],
|
127
|
+
pattern:
|
128
|
+
"^(Seconds|Microseconds|Milliseconds|Bytes|Kilobytes|Megabytes|Gigabytes|Terabytes|Bits|Kilobits|Megabits|Gigabits|Terabits|Percent|Count|Bytes\\/Second|Kilobytes\\/Second|Megabytes\\/Second|Gigabytes\\/Second|Terabytes\\/Second|Bits\\/Second|Kilobits\\/Second|Megabits\\/Second|Gigabits\\/Second|Terabits\\/Second|Count\\/Second|None)$",
|
129
|
+
},
|
130
|
+
StorageResolution: {
|
131
|
+
$id: "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/StorageResolution",
|
132
|
+
type: "integer",
|
133
|
+
title: "StorageResolution",
|
134
|
+
examples: [60],
|
135
|
+
},
|
136
|
+
},
|
137
|
+
},
|
138
|
+
},
|
139
|
+
},
|
140
|
+
},
|
141
|
+
},
|
142
|
+
},
|
143
|
+
},
|
144
|
+
},
|
145
|
+
};
|
146
|
+
|
147
|
+
const validateEmf = ajv.compile(emfSchema);
|
148
|
+
//sort customerDimensions key values in alphabetical order
|
149
|
+
const sortedDimensions: { [key: string]: string } = {};
|
150
|
+
for (
|
151
|
+
let i = 0;
|
152
|
+
i < Object.keys(CustomerDefinedDimension).sort().length;
|
153
|
+
i++
|
154
|
+
) {
|
155
|
+
sortedDimensions[Object.keys(CustomerDefinedDimension).sort()[i]] =
|
156
|
+
CustomerDefinedDimension[Object.keys(CustomerDefinedDimension).sort()[i]];
|
157
|
+
}
|
158
|
+
//if Object with Namespace and Dimensions already exists in Set
|
159
|
+
let check = cache[`${metricNamespace}${sortedDimensions}`];
|
160
|
+
if (check != undefined) {
|
161
|
+
//push the metrics object to Metrics array
|
162
|
+
cache[`${metricNamespace}${sortedDimensions}`]["_aws"][
|
163
|
+
"CloudWatchMetrics"
|
164
|
+
][0]["Metrics"].push({
|
165
|
+
Name: metricName,
|
166
|
+
Unit: metricUnitLabel,
|
167
|
+
StorageResolution: resolution,
|
168
|
+
});
|
169
|
+
//add key value to Log
|
170
|
+
check[`${metricName}`] = trackedVariable;
|
171
|
+
} else {
|
172
|
+
// //create new Structured Log and add it to cachedStructuredLogs - BMA 1/18/25 removed to test Ajv
|
173
|
+
// cache[`${metricNamespace}${sortedDimensions}`] = Object.assign(
|
174
|
+
// NameSpace & Dimensions for EMF part don't exist yet. Initialize variable to capture EMF/aws key:value pair
|
175
|
+
const newEmfLog = Object.assign(
|
176
|
+
{
|
177
|
+
_aws: {
|
178
|
+
Timestamp: Date.now(),
|
179
|
+
CloudWatchMetrics: [
|
180
|
+
{
|
181
|
+
Namespace: metricNamespace,
|
182
|
+
Dimensions: [Object.keys(sortedDimensions)],
|
183
|
+
Metrics: [
|
184
|
+
{
|
185
|
+
Name: metricName,
|
186
|
+
Unit: metricUnitLabel,
|
187
|
+
StorageResolution: resolution,
|
188
|
+
},
|
189
|
+
],
|
190
|
+
},
|
191
|
+
],
|
192
|
+
},
|
193
|
+
[`${metricName}`]: trackedVariable,
|
194
|
+
},
|
195
|
+
CustomerDefinedDimension
|
196
|
+
);
|
197
|
+
|
198
|
+
// Log the Unit value before validation
|
199
|
+
console.log(
|
200
|
+
"index.ts - Unit value before validation: ",
|
201
|
+
newEmfLog._aws.CloudWatchMetrics[0].Metrics[0].Unit
|
202
|
+
);
|
203
|
+
|
204
|
+
// validate the new EMF JSON schema against AWS EMF JSON schema before adding to cache object
|
205
|
+
const isValid = validateEmf(newEmfLog);
|
206
|
+
// //troubleshooting console.error in test
|
207
|
+
// console.log('index.ts - Validation result: ', isValid);
|
208
|
+
// troubleshooting console.error in emf test
|
209
|
+
// console.log('index.ts - Validation errors: ', validateEmf.errors);
|
210
|
+
// if it fails validation throw error
|
211
|
+
if (!isValid) {
|
212
|
+
console.error(
|
213
|
+
"An error occurred during EMF validation: ",
|
214
|
+
validateEmf.errors
|
215
|
+
);
|
216
|
+
throw new Error(
|
217
|
+
"Supplied/Proposed structured log does not comply with EMF schema"
|
218
|
+
);
|
219
|
+
}
|
220
|
+
// If it passes then add to cache object
|
221
|
+
cache[`${metricNamespace}${sortedDimensions}`] = newEmfLog;
|
222
|
+
}
|
223
|
+
|
224
|
+
if (deploy) {
|
225
|
+
//after last catalog function is invoked, send all cached logs with logger at once
|
226
|
+
for (let i = 0; i < Object.keys(cache).length; i++) {
|
227
|
+
logger.info(
|
228
|
+
`Your EMF compliant Structured Metrics Log ${i + 1}`,
|
229
|
+
cache[Object.keys(cache)[i]]
|
230
|
+
);
|
231
|
+
}
|
232
|
+
//clear cache
|
233
|
+
console.log("BEFORE:", cache);
|
234
|
+
for (var member in cache) delete cache[member];
|
235
|
+
console.log("AFTER:", cache);
|
236
|
+
}
|
237
|
+
}
|
238
|
+
export { cache, catalog };
|
239
|
+
|
240
|
+
/*Current Working logger invocation
|
241
|
+
logger.info("Your EMF compliant Structured Metrics Log",
|
242
|
+
Object.assign({
|
243
|
+
_aws: {
|
244
|
+
Timestamp: Date.now(),
|
245
|
+
CloudWatchMetrics: [
|
246
|
+
{
|
247
|
+
Namespace: metricNamespace,
|
248
|
+
Dimensions: [Object.keys(CustomerDefinedDimension)],
|
249
|
+
Metrics: [
|
250
|
+
{
|
251
|
+
Name: metricName,
|
252
|
+
Unit: metricUnitLabel,
|
253
|
+
StorageResolution: resolution,
|
254
|
+
}
|
255
|
+
]
|
256
|
+
}
|
257
|
+
]
|
258
|
+
},
|
259
|
+
[`${metricName}`]: trackedVariable,
|
260
|
+
},
|
261
|
+
CustomerDefinedDimension
|
262
|
+
)
|
263
|
+
|
264
|
+
)
|
265
|
+
*/
|
266
|
+
|
267
|
+
//Old handler function
|
268
|
+
// export const handler = async (_event, _context): Promise<void> => {
|
269
|
+
// const testObj = {
|
270
|
+
// testguy: "hi",
|
271
|
+
// fool: 42,
|
272
|
+
// functionVersion: "$LATEST",
|
273
|
+
// _aws: {
|
274
|
+
// Timestamp: Date.now(),
|
275
|
+
// CloudWatchMetrics: [
|
276
|
+
// {
|
277
|
+
// Namespace: "lambda-function-metrics",
|
278
|
+
// Dimensions: [["functionVersion"]],
|
279
|
+
// Metrics: [
|
280
|
+
// {
|
281
|
+
// Name: "fool",
|
282
|
+
// Unit: "Milliseconds",
|
283
|
+
// StorageResolution: 60
|
284
|
+
// }
|
285
|
+
// ]
|
286
|
+
// }
|
287
|
+
// ]
|
288
|
+
// },
|
289
|
+
// }
|
290
|
+
// logger.info(JSON.stringify(testObj));
|
291
|
+
// };
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<!-- Used by HTMLWebpackPlugin (per plugins config in webpack.config.js)
|
2
|
+
to build index.html with each webpack rebuild/rebundle -->
|
3
|
+
|
4
|
+
<!DOCTYPE html>
|
5
|
+
<html>
|
6
|
+
<head>
|
7
|
+
<meta charset="utf-8" />
|
8
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
9
|
+
<title>%= htmlWebpackPlugin.options.title %</title>
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
<h1 class="text-4xl text-blue-700">My Webpack + Tailwind App</h1>
|
14
|
+
<script src="dist/bundle.js"></script>
|
15
|
+
<!-- Insert React App here -->
|
16
|
+
<div id="root"></div>
|
17
|
+
</body>
|
18
|
+
</html>
|