diff --git a/README.md b/README.md index baa0cff..8d59bc2 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,11 @@ CustomMetrics is a NodeJS library to emit and query custom metrics for AWS apps. ## Background -AWS CloudWatch offers metrics to monitor specific aspects of your apps that are not covered by the default AWS infrastructure metrics. +AWS CloudWatch offers metrics to monitor AWS services and your apps. Unfortunately, custom AWS CloudWatch metrics can be very expensive. If updated or queried regularly, each each custom AWS CloudWatch metric may cost up to $3.60 per metric per year with additional costs for querying. If you have many metrics or high dimensionality on your metrics, this can lead to a very large CloudWatch Metrics bill. This high cost prevents using metrics liberally in your app at scale. -Unfortunately, AWS CloudWatch metrics can be very expensive. If updated or queried regularly, each each custom AWS CloudWatch metric may cost up to $3.60 per metric per year with additional costs for querying. If you have many metrics or high dimensionality on your metrics, this can lead to a very large CloudWatch Metrics bill. +> **CustomMetrics** provides low cost metrics that are much cheaper and faster than standard AWS CloudWatch metrics. -> **CustomMetrics** provides cost effective metrics that are much cheaper and faster than standard CloudWatch metrics. - -CustomMetrics achieves dramatic savings by supporting only **latest** period metrics. i.e. last day, last month, last hour, last 5 minutes etc. This enables each metric to be saved, stored and queried with minimal cost. +CustomMetrics achieves dramatic savings by supporting **latest** period metrics. i.e. last day, last month, last hour, last 5 minutes etc. This enables each metric to be saved, stored and queried with minimal cost. CustomMetrics stores metrics to a DynamoDB table of your choosing that can coexist with existing application data. @@ -34,6 +32,7 @@ CustomMetrics stores metrics to a DynamoDB table of your choosing that can coexi - Computes statistics for: average, min, max, count and sum. - Computes P value statistics with configurable P value resolution. - Supports a default metric intervals of: last 5 mins, hour, day, week, month and year. +- Supports querying from arbitrary start dates. - Configurable custom intervals for higher or different metric intervals. - Fast and flexible metric query API. - Query API can return data points or aggregate metric data to a single statistic. @@ -265,9 +264,11 @@ const metrics = new CustomMetrics({ }) ``` -CustomMetric spans define how each metric is processed and aged. The spans are an ordered list of metric interval periods. For example, the default spans calculate statistics for the periods: 5 minutes, 1 hour, 1 day, 1 week, 1 month and 1 year. +CustomMetric spans define how each metric is processed and aged. The spans are an ordered list of metric interval periods. + +The default spans store statistics for the periods: 5 minutes, 1 hour, 1 day, 1 week, 1 month and 1 year. -Via the `spans` constructor option you can provide an alternate list of spans for higher, lower or more granular resolution. +Via the `spans` CustomMetrics constructor you can provide an alternate list of spans for higher, lower or more granular resolution. The default CustomMetrics spans are: @@ -298,6 +299,26 @@ const metrics = new CustomMetrics({ }) ``` +#### Upgrading Metric Spans + +If you want to change your spans in the future, you can upgrade your metric data with new spans. To do this, you can use the `upgrade` method. This will apportion data points from the old spans to the new spans. If the number of samples in a span is increased, the data points will be apportioned across the new span. + +WARNING: if you define new spans via the constructor and do not call upgrade, the results may be unpredictable. If you do upgrade your metrics, ensure you supply the new span definition to all your constructor calls going forward. Otherwise, the default spans will be used. + +```typescript +const metrics = new CustomMetrics({ + table: 'mytable', + // New spans + spans: [ + {period: 24 * 60 * 60, samples: 24}, + {period: 7 * 24 * 60 * 60, samples: 28}, + {period: 28 * 24 * 60 * 60, samples: 28}, + {period: 365 * 24 * 60 * 60, samples: 48}, + ] +}) +await metrics.upgrade() +``` + ## Logging If the `log` constructor option is set to true, CustomMetrics will log errors to the console. If set to "verbose", CustomMetrics will also trace metric database accesses to the console. diff --git a/dist/cjs/index.d.ts b/dist/cjs/index.d.ts index 64410f0..d93a591 100644 --- a/dist/cjs/index.d.ts +++ b/dist/cjs/index.d.ts @@ -98,6 +98,7 @@ export type MetricQueryOptions = { id?: string; log?: boolean; owner?: string; + start?: number; timestamp?: number; }; type BufferElt = { diff --git a/dist/cjs/index.js b/dist/cjs/index.js index dc20784..d56b76d 100644 --- a/dist/cjs/index.js +++ b/dist/cjs/index.js @@ -243,14 +243,31 @@ class CustomMetrics { if (!metric) { return { dimensions, id: options.id, metric: metricName, namespace, period, points: [], owner, samples: 0 }; } - let span = metric.spans.find((s) => period <= s.period); - if (!span) { - span = metric.spans[metric.spans.length - 1]; - period = span.period; + let span; + if (options.start) { + span = metric.spans.find((s) => (s.end - s.period) <= options.start / 1000); + if (!span) { + span = metric.spans[metric.spans.length - 1]; + period = span.period; + } + } + else { + span = metric.spans.find((s) => period <= s.period); + if (!span) { + span = metric.spans[metric.spans.length - 1]; + period = span.period; + } } this.addValue(metric, timestamp, { count: 0, sum: 0 }, 0, period); let result; if (metric && span) { + if (options.start) { + let interval = span.period / span.samples; + let end = span.points.length - Math.ceil((span.end - (options.start / 1000 + period)) / interval); + let front = end - Math.round(period / interval); + span.end -= (span.points.length - end) * interval; + span.points = span.points.slice(front, end); + } if (options.accumulate) { result = this.accumulateMetric(metric, span, statistic, owner, timestamp); } diff --git a/dist/mjs/index.d.ts b/dist/mjs/index.d.ts index 64410f0..d93a591 100644 --- a/dist/mjs/index.d.ts +++ b/dist/mjs/index.d.ts @@ -98,6 +98,7 @@ export type MetricQueryOptions = { id?: string; log?: boolean; owner?: string; + start?: number; timestamp?: number; }; type BufferElt = { diff --git a/dist/mjs/index.js b/dist/mjs/index.js index 349e219..1005abb 100644 --- a/dist/mjs/index.js +++ b/dist/mjs/index.js @@ -251,14 +251,31 @@ export class CustomMetrics { if (!metric) { return { dimensions, id: options.id, metric: metricName, namespace, period, points: [], owner, samples: 0 }; } - let span = metric.spans.find((s) => period <= s.period); - if (!span) { - span = metric.spans[metric.spans.length - 1]; - period = span.period; + let span; + if (options.start) { + span = metric.spans.find((s) => (s.end - s.period) <= options.start / 1000); + if (!span) { + span = metric.spans[metric.spans.length - 1]; + period = span.period; + } + } + else { + span = metric.spans.find((s) => period <= s.period); + if (!span) { + span = metric.spans[metric.spans.length - 1]; + period = span.period; + } } this.addValue(metric, timestamp, { count: 0, sum: 0 }, 0, period); let result; if (metric && span) { + if (options.start) { + let interval = span.period / span.samples; + let end = span.points.length - Math.ceil((span.end - (options.start / 1000 + period)) / interval); + let front = end - Math.round(period / interval); + span.end -= (span.points.length - end) * interval; + span.points = span.points.slice(front, end); + } if (options.accumulate) { result = this.accumulateMetric(metric, span, statistic, owner, timestamp); } diff --git a/test/utils/setup.ts b/test/utils/setup.ts index 62d9b01..5fed3d8 100644 --- a/test/utils/setup.ts +++ b/test/utils/setup.ts @@ -2,8 +2,8 @@ Setup -- setup for the test run */ import {DynamoDBClient} from '@aws-sdk/client-dynamodb' -import {CreateTableCommand, DescribeTableCommand} from '@aws-sdk/client-dynamodb' -import DynamoDbLocal from 'dynamo-db-local' +import {CreateTableCommand, CreateTableCommandInput, DescribeTableCommand} from '@aws-sdk/client-dynamodb' +import * as DynamoDbLocal from 'dynamo-db-local' import waitPort from 'wait-port' const PORT = parseInt(process.env.PORT || '4765') @@ -41,7 +41,7 @@ module.exports = async () => { } async function createTable(client, table) { - let def = { + let def: CreateTableCommandInput = { AttributeDefinitions: [ {AttributeName: 'pk', AttributeType: 'S'}, {AttributeName: 'sk', AttributeType: 'S'},