Metrics creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF).
These metrics can be visualized through Amazon CloudWatch Console.
Key features¶If you're new to Amazon CloudWatch, there are two terminologies you must be aware of before using this utility:
ServerlessEcommerce
.ColdStart
metric by Payment service
.Install the library in your project:
npm install @aws-lambda-powertools/metrics
Caution
When using the Lambda Advanced Logging Controls feature you must install version of Powertools for AWS Lambda (TypeScript) v1.15.0 or newer.
Usage¶The Metrics
utility must always be instantiated outside of the Lambda handler. In doing this, subsequent invocations processed by the same instance of your function can reuse these resources. This saves cost by reducing function run time. In addition, Metrics
can track cold start and emit the appropriate metrics.
handler.ts
1 2 3 4 5 6 7 8 9 10 11 12 13
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
Utility settings¶
The library requires two settings. You can set them as environment variables, or pass them in the constructor.
These settings will be used across all metrics emitted:
Setting Description Environment variable Default Allowed Values Example Constructor parameter Service Optionally, sets service metric dimension across all metricsPOWERTOOLS_SERVICE_NAME
service_undefined
Any string serverlessAirline
serviceName
Metric namespace Logical container where all metrics will be placed POWERTOOLS_METRICS_NAMESPACE
default_namespace
Any string serverlessAirline
default_namespace
Function Name Function name used as dimension for the ColdStart
metric POWERTOOLS_METRICS_FUNCTION_NAME
See docs Any string my-function-name
functionName
Disabled Whether to disable the log of metrics to standard output or not POWERTOOLS_METRICS_DISABLED
false
Boolean true
Tip
Use your application name or main service as the metric namespace to easily group all metrics
Example using AWS Serverless Application Model (SAM)¶The Metrics
utility is instantiated outside of the Lambda handler. In doing this, the same instance can be used across multiple invocations inside the same execution environment. This allows Metrics
to be aware of things like whether or not a given invocation had a cold start or not.
handler.tstemplate.yml
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
// Metrics parameters fetched from the environment variables (see template.yaml tab)
const metrics = new Metrics();
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
// You can also pass the parameters in the constructor
// const metrics = new Metrics({
// namespace: 'serverlessAirline',
// serviceName: 'orders'
// });
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs22.x
Environment:
Variables:
POWERTOOLS_SERVICE_NAME: orders
POWERTOOLS_METRICS_NAMESPACE: serverlessAirline
POWERTOOLS_METRICS_FUNCTION_NAME: my-function-name
You can initialize Metrics anywhere in your code - It'll keep track of your aggregate metrics in memory.
Creating metrics¶You can create metrics using the addMetric
method, and you can create dimensions for all your aggregate metrics using the addDimension
method.
MetricsMetrics with custom dimensions
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
metrics.publishStoredMetrics();
};
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addDimension('environment', 'prod');
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
metrics.publishStoredMetrics();
};
Creating dimension sets¶
You can create separate dimension sets for your metrics using the addDimensions
method. This allows you to group metrics by different dimension combinations.
When you call addDimensions()
, it creates a new dimension set rather than adding to the existing dimensions. This is useful when you want to track the same metric across different dimension combinations.
handler.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
// Add a single dimension
metrics.addDimension('environment', 'prod');
// Add a new dimension set
metrics.addDimensions({
dimension1: '1',
dimension2: '2',
});
// Add another dimension set
metrics.addDimensions({
region: 'us-east-1',
category: 'books',
});
// Add metrics
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
metrics.publishStoredMetrics();
};
Autocomplete Metric Units
Use the MetricUnit
enum to easily find a supported metric unit by CloudWatch. Alternatively, you can pass the value as a string if you already know them e.g. "Count".
Metrics overflow
CloudWatch EMF supports a max of 100 metrics per batch. Metrics will automatically propagate all the metrics when adding the 100th metric. Subsequent metrics, e.g. 101th, will be aggregated into a new EMF object, for your convenience.
Do not create metrics or dimensions outside the handler
Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behavior.
Adding high-resolution metrics¶You can create high-resolution metrics passing resolution
as parameter to addMetric
.
When is it useful?
High-resolution metrics are data with a granularity of one second and are very useful in several situations such as telemetry, time series, real-time incident management, and others.
Metrics with high resolution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import {
MetricResolution,
Metrics,
MetricUnit,
} from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric(
'successfulBooking',
MetricUnit.Count,
1,
MetricResolution.High
);
};
Autocomplete Metric Resolutions
Use the MetricResolution
type to easily find a supported metric resolution by CloudWatch. Alternatively, you can pass the allowed values of 1 or 60 as an integer.
You can call addMetric()
with the same name multiple times. The values will be grouped together in an array.
addMetric() with the same nameExample CloudWatch Logs excerpt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('performedActionA', MetricUnit.Count, 2);
// do something else...
metrics.addMetric('performedActionA', MetricUnit.Count, 1);
};
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
{
"performedActionA": [
2,
1
],
"_aws": {
"Timestamp": 1592234975665,
"CloudWatchMetrics": [
{
"Namespace": "serverlessAirline",
"Dimensions": [
[
"service"
]
],
"Metrics": [
{
"Name": "performedActionA",
"Unit": "Count"
}
]
}
]
},
"service": "orders"
}
Adding default dimensions¶
You can add default dimensions to your metrics by passing them as parameters in 4 ways:
setDefaultDimensions
methodconstructorMiddy middlewaresetDefaultDimensions methodwith logMetrics decorator
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
defaultDimensions: { environment: 'prod', foo: 'bar' },
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
const lambdaHandler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
// Wrap the handler with middy
export const handler = middy(lambdaHandler)
// Use the middleware by passing the Metrics instance as a parameter
.use(
logMetrics(metrics, {
defaultDimensions: { environment: 'prod', foo: 'bar' },
})
);
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
metrics.setDefaultDimensions({ environment: 'prod', foo: 'bar' });
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
const DEFAULT_DIMENSIONS = { environment: 'prod', foo: 'bar' };
export class Lambda implements LambdaInterface {
// Decorate your handler class method
@metrics.logMetrics({ defaultDimensions: DEFAULT_DIMENSIONS })
public async handler(_event: unknown, _context: unknown): Promise<void> {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
}
}
const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass); // (1)
this
within the class methods.If you'd like to remove them at some point, you can use the clearDefaultDimensions
method.
When creating metrics, we use the current timestamp. If you want to change the timestamp of all the metrics you create, utilize the setTimestamp
function. You can specify a datetime object or an integer representing an epoch timestamp in milliseconds.
Note that when specifying the timestamp using an integer, it must adhere to the epoch timezone format in milliseconds.
setTimestamp method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
metrics.setTimestamp(metricTimestamp);
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
Flushing metrics¶
As you finish adding all your metrics, you need to serialize and "flush them" by calling publishStoredMetrics()
. This will print the metrics to standard output.
You can flush metrics automatically using one of the following methods:
Using the Middy middleware or decorator will automatically validate, serialize, and flush all your metrics. During metrics validation, if no metrics are provided then a warning will be logged, but no exception will be thrown. If you do not use the middleware or decorator, you have to flush your metrics manually.
Metric validation
If metrics are provided, and any of the following criteria are not met, a RangeError
error will be thrown:
See below an example of how to automatically flush metrics with the Middy-compatible logMetrics
middleware.
handler.tsExample CloudWatch Logs excerpt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
const lambdaHandler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
export const handler = middy(lambdaHandler).use(logMetrics(metrics));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
{
"successfulBooking": 1.0,
"_aws": {
"Timestamp": 1592234975665,
"CloudWatchMetrics": [{
"Namespace": "serverlessAirline",
"Dimensions": [
[ "service" ]
],
"Metrics": [{
"Name": "successfulBooking",
"Unit": "Count"
}]
}]
},
"service": "orders"
}
Using the class decorator¶
Note
The class method decorators in this project follow the experimental implementation enabled via the experimentalDecorators
compiler option in TypeScript.
Additionally, they are implemented to decorate async methods. When decorating a synchronous one, the decorator replaces its implementation with an async one causing the caller to have to await
the now decorated method.
If this is not the desired behavior, you can use the logMetrics
middleware instead.
The logMetrics
decorator of the metrics utility can be used when your Lambda handler function is implemented as method of a Class.
handler.tsExample CloudWatch Logs excerpt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
class Lambda implements LambdaInterface {
@metrics.logMetrics()
public async handler(_event: unknown, _context: unknown): Promise<void> {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
}
}
const handlerClass = new Lambda();
export const handler = handlerClass.handler.bind(handlerClass); // (1)
this
within the class methods.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
{
"successfulBooking": 1.0,
"_aws": {
"Timestamp": 1592234975665,
"CloudWatchMetrics": [{
"Namespace": "successfulBooking",
"Dimensions": [
[ "service" ]
],
"Metrics": [{
"Name": "successfulBooking",
"Unit": "Count"
}]
}]
},
"service": "orders"
}
Manually¶
You can manually flush the metrics with publishStoredMetrics
as follows:
Warning
Metrics, dimensions and namespace validation still applies.
handler.tsExample CloudWatch Logs excerpt
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 10);
metrics.publishStoredMetrics();
};
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
{
"successfulBooking": 1.0,
"_aws": {
"Timestamp": 1592234975665,
"CloudWatchMetrics": [{
"Namespace": "successfulBooking",
"Dimensions": [
[ "service" ]
],
"Metrics": [{
"Name": "successfulBooking",
"Unit": "Count"
}]
}]
},
"service": "orders"
}
Throwing a RangeError when no metrics are emitted¶
If you want to ensure that at least one metric is emitted before you flush them, you can use the throwOnEmptyMetrics
parameter and pass it to the middleware or decorator:
handler.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
const lambdaHandler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
export const handler = middy(lambdaHandler).use(
logMetrics(metrics, { throwOnEmptyMetrics: true })
);
Capturing a cold start invocation as metric¶
You can optionally capture cold start metrics with the logMetrics
middleware or decorator via the captureColdStartMetric
param.
Middy MiddlewarelogMetrics decorator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
const lambdaHandler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
export const handler = middy(lambdaHandler).use(
logMetrics(metrics, { captureColdStartMetric: true })
);
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export class MyFunction implements LambdaInterface {
@metrics.logMetrics({ captureColdStartMetric: true })
public async handler(_event: unknown, _context: unknown): Promise<void> {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
}
}
If it's a cold start invocation, this feature will:
ColdStart
function_name
, service
and default dimensionsThis has the advantage of keeping cold start metric separate from your application metrics, where you might have unrelated dimensions.
We do not emit 0 as a value for the ColdStart metric for cost-efficiency reasons. Let us know if you'd prefer a flag to override it.
Setting function name¶When emitting cold start metrics, the function_name
dimension defaults to context.functionName
. If you want to change the value you can set the functionName
parameter in the metrics constructor, define the environment variable POWERTOOLS_METRICS_FUNCTION_NAME
, or pass a value to captureColdStartMetric
.
The priority of the function_name
dimension value is defined as:
functionName
constructor optionPOWERTOOLS_METRICS_FUNCTION_NAME
environment variablecaptureColdStartMetric
call, or context.functionName
if using logMetrics decorator or Middy middlewareconstructorcaptureColdStartMetric method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
functionName: 'my-function-name',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.captureColdStartMetric();
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
metrics.publishStoredMetrics();
};
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.captureColdStartMetric('my-function-name');
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
metrics.publishStoredMetrics();
};
Advanced¶ Adding metadata¶
You can add high-cardinality data as part of your Metrics log with the addMetadata
method. This is useful when you want to search highly contextual information along with your metrics in your logs.
Info
This will not be available during metrics visualization - Use dimensions for this purpose
handler.tsExample CloudWatch Logs excerpt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
import { logMetrics } from '@aws-lambda-powertools/metrics/middleware';
import middy from '@middy/core';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
const lambdaHandler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
metrics.addMetadata('bookingId', '7051cd10-6283-11ec-90d6-0242ac120003');
};
export const handler = middy(lambdaHandler).use(logMetrics(metrics));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
{
"successfulBooking": 1.0,
"_aws": {
"Timestamp": 1592234975665,
"CloudWatchMetrics": [{
"Namespace": "serverlessAirline",
"Dimensions": [
[ "service" ]
],
"Metrics": [{
"Namespace": "exampleApplication",
"Dimensions": [
[ "service" ]
],
"Metrics": [{
"Name": "successfulBooking",
"Unit": "Count"
}]
}]
}]
},
"service": "orders",
"bookingId": "7051cd10-6283-11ec-90d6-0242ac120003"
}
Single metric with different dimensions¶
CloudWatch EMF uses the same dimensions across all your metrics. Use singleMetric
if you have a metric that should have different dimensions.
Generally, using different dimensions would be an edge case since you pay for unique metric.
Keep the following formula in mind: unique metric = (metric_name + dimension_name + dimension_value)
.
Single Metric
1 2 3 4 5 6 7 8 9 10 11 12 13
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});
export const handler = async (event: { orderId: string }) => {
const singleMetric = metrics.singleMetric();
singleMetric.addDimension('metricType', 'business');
singleMetric.addMetadata('orderId', event.orderId); // (1)!
singleMetric.addMetric('successfulBooking', MetricUnit.Count, 1); // (2)!
};
addMetric()
to ensure it's included in the same EMF blob.addMetric()
is called, so you don't need to call publishStoredMetrics()
.You can customize how Metrics logs warnings and debug messages to standard output by passing a custom logger as a constructor parameter. This is useful when you want to silence warnings or debug messages, or when you want to log them to a different output.
Custom logger
1 2 3 4 5 6 7 8 9 10 11 12
import { Logger, LogLevel } from '@aws-lambda-powertools/logger';
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const logger = new Logger({ logLevel: LogLevel.CRITICAL });
const metrics = new Metrics({
serviceName: 'serverless-airline',
namespace: 'orders',
singleMetric: true,
logger,
});
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
Testing your code¶
When unit testing your code that uses the Metrics utility, you may want to silence the logs emitted by the utility. To do so, you can set the POWERTOOLS_DEV
environment variable to true
. This instructs the utility to not emit any logs to standard output.
If instead you want to spy on the logs emitted by the utility, you must set the POWERTOOLS_DEV
environment variable to true
in conjunction with the POWERTOOLS_METRICS_DISABLED
environment variable set to false
.
When POWERTOOLS_DEV
is enabled, Metrics uses the global console
to emit metrics to standard out. This allows you to easily spy on the logs emitted and make assertions on them.
Spying on emitted metrics
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
import { describe, expect, it, vi } from 'vitest';
vi.hoisted(() => {
process.env.POWERTOOLS_DEV = 'true';
process.env.POWERTOOLS_METRICS_DISABLED = 'false';
});
describe('Metrics tests', () => {
it('emits metrics properly', async () => {
// Prepare
const metricsEmittedSpy = vi
.spyOn(console, 'log')
.mockImplementation(() => {});
// Act
// ...
// Assess
expect(metricsEmittedSpy).toHaveBeenCalledOnce();
});
});
2025-08-14
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4