enh nodeexporter filtering (#1201)

This commit is contained in:
Colin Gagnaire 2018-10-29 13:21:48 +01:00 committed by GitHub
parent 93a41ca33e
commit eb9691b4ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 184 additions and 108 deletions

View File

@ -80,7 +80,8 @@ sub new {
$self->{version} = '1.0';
$options{options}->add_options(arguments =>
{
"node:s" => { name => 'node', default => '.*' },
"instance:s" => { name => 'instance', default => 'instance=~".*"' },
"cpu:s" => { name => 'cpu', default => 'cpu=~".*"' },
"extra-filter:s@" => { name => 'extra_filter' },
"metric-overload:s@" => { name => 'metric_overload' },
});
@ -95,12 +96,25 @@ sub check_options {
$self->{metrics} = {
'cpu' => "^node_cpu.*",
};
foreach my $metric (@{$self->{option_results}->{metric_overload}}) {
next if ($metric !~ /(.*),(.*)/);
$self->{metrics}->{$1} = $2 if (defined($self->{metrics}->{$1}));
}
$self->{labels} = {};
foreach my $label (('instance', 'cpu')) {
if ($self->{option_results}->{$label} !~ /^(\w+)[!~=]+\".*\"$/) {
$self->{output}->add_option_msg(short_msg => "Need to specify --" . $label . " option as a PromQL filter.");
$self->{output}->option_exit();
}
$self->{labels}->{$label} = $1;
}
$self->{extra_filter} = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$self->{extra_filter} .= ',' . $filter;
}
$self->{prom_timeframe} = defined($self->{option_results}->{timeframe}) ? $self->{option_results}->{timeframe} : 900;
$self->{prom_step} = defined($self->{option_results}->{step}) ? $self->{option_results}->{step} : "1m";
}
@ -111,22 +125,20 @@ sub manage_selection {
$self->{nodes} = {};
$self->{cpu} = {};
my $extra_filter = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$extra_filter .= ',' . $filter;
}
my $results = $options{custom}->query_range(queries => [ '(1 - irate({__name__=~"' . $self->{metrics}->{cpu} . '",mode="idle",instance=~"' . $self->{option_results}->{node} .
'"' . $extra_filter . '}[1m])) * 100' ],
my $results = $options{custom}->query_range(queries => [ '(1 - irate({__name__=~"' . $self->{metrics}->{cpu} . '",' .
'mode="idle",' .
$self->{option_results}->{instance} . ',' .
$self->{option_results}->{cpu} .
$self->{extra_filter} . '}[1m])) * 100' ],
timeframe => $self->{prom_timeframe}, step => $self->{prom_step});
foreach my $metric (@{$results}) {
my $average = $options{custom}->compute(aggregation => 'average', values => $metric->{values});
$self->{nodes}->{$metric->{metric}->{instance}}->{display} = $metric->{metric}->{instance};
$self->{nodes}->{$metric->{metric}->{instance}}->{average} += $average;
$self->{nodes}->{$metric->{metric}->{instance}}->{cpu}->{$metric->{metric}->{cpu}}->{multi} = $metric->{metric}->{instance};
$self->{nodes}->{$metric->{metric}->{instance}}->{cpu}->{$metric->{metric}->{cpu}}->{display} = $metric->{metric}->{cpu};
$self->{nodes}->{$metric->{metric}->{instance}}->{cpu}->{$metric->{metric}->{cpu}}->{average} = $average;
foreach my $result (@{$results}) {
my $average = $options{custom}->compute(aggregation => 'average', values => $result->{values});
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{display} = $result->{metric}->{$self->{labels}->{instance}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{average} += $average;
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{cpu}->{$result->{metric}->{$self->{labels}->{cpu}}}->{multi} = $result->{metric}->{$self->{labels}->{instance}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{cpu}->{$result->{metric}->{$self->{labels}->{cpu}}}->{display} = $result->{metric}->{$self->{labels}->{cpu}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{cpu}->{$result->{metric}->{$self->{labels}->{cpu}}}->{average} = $average;
}
foreach my $node (keys %{$self->{nodes}}) {
@ -149,9 +161,13 @@ Check CPU usage for nodes and each of their cores.
=over 8
=item B<--node>
=item B<--instance>
Filter on a specific node (Must be a regexp, Default: '.*')
Filter on a specific instance (Must be a PromQL filter, Default: 'instance=~".*"')
=item B<--cpu>
Filter on a specific cpu (Must be a PromQL filter, Default: 'cpu=~".*"')
=item B<--warning-*>

View File

@ -29,8 +29,10 @@ sub set_counters {
my ($self, %options) = @_;
$self->{maps_counters_type} = [
{ name => 'nodes', type => 3, cb_prefix_output => 'prefix_nodes_output', message_multiple => 'All nodes CPU usage are ok',
counters => [ { name => 'cpu', type => 1, cb_prefix_output => 'prefix_cpu_output', message_multiple => 'All CPU usage are ok' } ] },
{ name => 'nodes', type => 3, cb_prefix_output => 'prefix_nodes_output',
message_multiple => 'All nodes CPU usage are ok', skipped_code => { -10 => 1 },
counters => [ { name => 'cpu', type => 1, cb_prefix_output => 'prefix_cpu_output',
message_multiple => 'All CPU usage are ok', skipped_code => { -10 => 1 } } ] },
];
$self->{maps_counters}->{nodes} = [
@ -219,7 +221,9 @@ sub new {
$self->{version} = '1.0';
$options{options}->add_options(arguments =>
{
"node:s" => { name => 'node', default => '.*' },
"instance:s" => { name => 'instance', default => 'instance=~".*"' },
"cpu:s" => { name => 'cpu', default => 'cpu=~".*"' },
"type:s" => { name => 'type', default => 'mode=~".*"' },
"extra-filter:s@" => { name => 'extra_filter' },
"metric-overload:s@" => { name => 'metric_overload' },
});
@ -234,12 +238,25 @@ sub check_options {
$self->{metrics} = {
'cpu' => "^node_cpu.*",
};
foreach my $metric (@{$self->{option_results}->{metric_overload}}) {
next if ($metric !~ /(.*),(.*)/);
$self->{metrics}->{$1} = $2 if (defined($self->{metrics}->{$1}));
}
$self->{labels} = {};
foreach my $label (('instance', 'cpu', 'type')) {
if ($self->{option_results}->{$label} !~ /^(\w+)[!~=]+\".*\"$/) {
$self->{output}->add_option_msg(short_msg => "Need to specify --" . $label . " option as a PromQL filter.");
$self->{output}->option_exit();
}
$self->{labels}->{$label} = $1;
}
$self->{extra_filter} = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$self->{extra_filter} .= ',' . $filter;
}
$self->{prom_timeframe} = defined($self->{option_results}->{timeframe}) ? $self->{option_results}->{timeframe} : 900;
$self->{prom_step} = defined($self->{option_results}->{step}) ? $self->{option_results}->{step} : "1m";
}
@ -248,23 +265,21 @@ sub manage_selection {
my ($self, %options) = @_;
$self->{nodes} = {};
my $extra_filter = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$extra_filter .= ',' . $filter;
}
my $results = $options{custom}->query_range(queries => [ '(irate({__name__=~"' . $self->{metrics}->{cpu} . '",instance=~"' . $self->{option_results}->{node} .
'"' . $extra_filter . '}[1m])) * 100' ],
my $results = $options{custom}->query_range(queries => [ '(irate({__name__=~"' . $self->{metrics}->{cpu} . '",' .
$self->{option_results}->{instance} . ',' .
$self->{option_results}->{cpu} . ',' .
$self->{option_results}->{type} .
$self->{extra_filter} . '}[1m])) * 100' ],
timeframe => $self->{prom_timeframe}, step => $self->{prom_step});
foreach my $metric (@{$results}) {
my $average = $options{custom}->compute(aggregation => 'average', values => $metric->{values});
$self->{nodes}->{$metric->{metric}->{instance}}->{display} = $metric->{metric}->{instance};
$self->{nodes}->{$metric->{metric}->{instance}}->{$metric->{metric}->{mode}} += $average;
$self->{nodes}->{$metric->{metric}->{instance}}->{cpu}->{$metric->{metric}->{cpu}}->{multi} = $metric->{metric}->{instance};
$self->{nodes}->{$metric->{metric}->{instance}}->{cpu}->{$metric->{metric}->{cpu}}->{display} = $metric->{metric}->{cpu};
$self->{nodes}->{$metric->{metric}->{instance}}->{cpu}->{$metric->{metric}->{cpu}}->{$metric->{metric}->{mode}} = $average;
foreach my $result (@{$results}) {
my $average = $options{custom}->compute(aggregation => 'average', values => $result->{values});
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{display} = $result->{metric}->{$self->{labels}->{instance}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{$result->{metric}->{$self->{labels}->{type}}} += $average;
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{cpu}->{$result->{metric}->{$self->{labels}->{cpu}}}->{multi} = $result->{metric}->{$self->{labels}->{instance}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{cpu}->{$result->{metric}->{$self->{labels}->{cpu}}}->{display} = $result->{metric}->{$self->{labels}->{cpu}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{cpu}->{$result->{metric}->{$self->{labels}->{cpu}}}->{$result->{metric}->{$self->{labels}->{type}}} = $average;
}
foreach my $node (keys %{$self->{nodes}}) {
@ -290,9 +305,17 @@ Check CPU detailed usage for nodes and each of their cores.
=over 8
=item B<--node>
=item B<--instance>
Filter on a specific node (Must be a regexp, Default: '.*')
Filter on a specific instance (Must be a PromQL filter, Default: 'instance=~".*"')
=item B<--cpu>
Filter on a specific cpu (Must be a PromQL filter, Default: 'cpu=~".*"')
=item B<--type>
Filter on a specific type (Must be a PromQL filter, Default: 'mode=~".*"')
=item B<--warning-*>

View File

@ -80,7 +80,7 @@ sub new {
$self->{version} = '1.0';
$options{options}->add_options(arguments =>
{
"node:s" => { name => 'node', default => '.*' },
"instance:s" => { name => 'instance', default => 'instance=~".*"' },
"extra-filter:s@" => { name => 'extra_filter' },
"metric-overload:s@" => { name => 'metric_overload' },
});
@ -97,11 +97,24 @@ sub check_options {
'load5' => '^node_load5$',
'load15' => '^node_load15$',
};
foreach my $metric (@{$self->{option_results}->{metric_overload}}) {
next if ($metric !~ /(.*),(.*)/);
$self->{metrics}->{$1} = $2 if (defined($self->{metrics}->{$1}));
}
$self->{labels} = {};
foreach my $label (('instance')) {
if ($self->{option_results}->{$label} !~ /^(\w+)[!~=]+\".*\"$/) {
$self->{output}->add_option_msg(short_msg => "Need to specify --" . $label . " option as a PromQL filter.");
$self->{output}->option_exit();
}
$self->{labels}->{$label} = $1;
}
$self->{extra_filter} = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$self->{extra_filter} .= ',' . $filter;
}
}
sub manage_selection {
@ -109,21 +122,19 @@ sub manage_selection {
$self->{nodes} = {};
my $extra_filter = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$extra_filter .= ',' . $filter;
}
my $results = $options{custom}->query(queries => [ 'label_replace({__name__=~"' . $self->{metrics}->{load1} . '",' .
$self->{option_results}->{instance} .
$self->{extra_filter} . '}, "__name__", "load1", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{load5} . '",' .
$self->{option_results}->{instance} .
$self->{extra_filter} . '}, "__name__", "load5", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{load15} . '",' .
$self->{option_results}->{instance} .
$self->{extra_filter} . '}, "__name__", "load15", "", "")' ]);
my $results = $options{custom}->query(queries => [ 'label_replace({__name__=~"' . $self->{metrics}->{load1} . '",instance=~"' . $self->{option_results}->{node} .
'"' . $extra_filter . '}, "__name__", "load1", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{load5} . '",instance=~"' . $self->{option_results}->{node} .
'"' . $extra_filter . '}, "__name__", "load5", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{load15} . '",instance=~"' . $self->{option_results}->{node} .
'"' . $extra_filter . '}, "__name__", "load15", "", "")' ]);
foreach my $metric (@{$results}) {
$self->{nodes}->{$metric->{metric}->{instance}}->{display} = $metric->{metric}->{instance};
$self->{nodes}->{$metric->{metric}->{instance}}->{$metric->{metric}->{__name__}} = ${$metric->{value}}[1];
foreach my $result (@{$results}) {
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{display} = $result->{metric}->{$self->{labels}->{instance}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{$result->{metric}->{__name__}} = ${$result->{value}}[1];
}
if (scalar(keys %{$self->{nodes}}) <= 0) {
@ -142,9 +153,9 @@ Check nodes load.
=over 8
=item B<--node>
=item B<--instance>
Filter on a specific node (Must be a regexp, Default: '.*')
Filter on a specific instance (Must be a PromQL filter, Default: 'instance=~".*"')
=item B<--warning-*>

View File

@ -139,9 +139,9 @@ sub new {
$self->{version} = '1.0';
$options{options}->add_options(arguments =>
{
"node:s" => { name => 'node', default => '.*' },
"extra-filter:s@" => { name => 'extra_filter' },
"instance:s" => { name => 'instance', default => 'instance=~".*"' },
"units:s" => { name => 'units', default => '%' },
"extra-filter:s@" => { name => 'extra_filter' },
"metric-overload:s@" => { name => 'metric_overload' },
});
@ -158,11 +158,24 @@ sub check_options {
'cached' => "^node_memory_Cached.*",
'buffer' => "^node_memory_Buffers.*",
};
foreach my $metric (@{$self->{option_results}->{metric_overload}}) {
next if ($metric !~ /(.*),(.*)/);
$self->{metrics}->{$1} = $2 if (defined($self->{metrics}->{$1}));
}
$self->{labels} = {};
foreach my $label (('instance')) {
if ($self->{option_results}->{$label} !~ /^(\w+)[!~=]+\".*\"$/) {
$self->{output}->add_option_msg(short_msg => "Need to specify --" . $label . " option as a PromQL filter.");
$self->{output}->option_exit();
}
$self->{labels}->{$label} = $1;
}
$self->{extra_filter} = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$self->{extra_filter} .= ',' . $filter;
}
$instance_mode = $self;
}
@ -172,23 +185,22 @@ sub manage_selection {
$self->{nodes} = {};
my $extra_filter = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$extra_filter .= ',' . $filter;
}
my $results = $options{custom}->query(queries => [ 'label_replace({__name__=~"' . $self->{metrics}->{total} . '",' .
$self->{option_results}->{instance} .
$self->{extra_filter} . '}, "__name__", "total", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{available} . '",' .
$self->{option_results}->{instance} .
$self->{extra_filter} . '}, "__name__", "available", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{cached} . '",' .
$self->{option_results}->{instance} .
$self->{extra_filter} . '}, "__name__", "cached", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{buffer} . '",' .
$self->{option_results}->{instance} .
$self->{extra_filter} . '}, "__name__", "buffer", "", "")' ]);
my $results = $options{custom}->query(queries => [ 'label_replace({__name__=~"' . $self->{metrics}->{total} . '",instance=~"' . $self->{option_results}->{node} .
'"' . $extra_filter . '}, "__name__", "total", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{available} . '",instance=~"' . $self->{option_results}->{node} .
'"' . $extra_filter . '}, "__name__", "available", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{cached} . '",instance=~"' . $self->{option_results}->{node} .
'"' . $extra_filter . '}, "__name__", "cached", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{buffer} . '",instance=~"' . $self->{option_results}->{node} .
'"' . $extra_filter . '}, "__name__", "buffer", "", "")' ]);
foreach my $metric (@{$results}) {
$self->{nodes}->{$metric->{metric}->{instance}}->{display} = $metric->{metric}->{instance};
$self->{nodes}->{$metric->{metric}->{instance}}->{$metric->{metric}->{__name__}} = ${$metric->{value}}[1];
foreach my $result (@{$results}) {
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{display} = $result->{metric}->{$self->{labels}->{instance}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{$result->{metric}->{__name__}} = ${$result->{value}}[1];
}
if (scalar(keys %{$self->{nodes}}) <= 0) {
@ -207,9 +219,9 @@ Check memory usage.
=over 8
=item B<--node>
=item B<--instance>
Filter on a specific node (Must be a regexp, Default: '.*')
Filter on a specific instance (Must be a PromQL filter, Default: 'instance=~".*"')
=item B<--warning-*>
@ -221,6 +233,10 @@ Can be: 'usage', 'buffer', 'cached'.
Threshold critical.
Can be: 'usage', 'buffer', 'cached'.
=item B<--units>
Units of thresholds (Default: '%') ('%', 'B').
=item B<--extra-filter>
Add a PromQL filter (Can be multiple)

View File

@ -140,12 +140,12 @@ sub new {
$self->{version} = '1.0';
$options{options}->add_options(arguments =>
{
"node:s" => { name => 'node', default => '.*' },
"storage:s" => { name => 'storage', default => '.*' },
"exclude-storage-type:s" => { name => 'exclude_storage_type', default => 'rootfs|tmpfs' },
"extra-filter:s@" => { name => 'extra_filter' },
"instance:s" => { name => 'instance', default => 'instance=~".*"' },
"mountpoint:s" => { name => 'mountpoint', default => 'mountpoint=~".*"' },
"fstype:s" => { name => 'fstype', default => 'fstype=~"overlay"' },
"units:s" => { name => 'units', default => '%' },
"free" => { name => 'free' },
"extra-filter:s@" => { name => 'extra_filter' },
"metric-overload:s@" => { name => 'metric_overload' },
});
@ -160,11 +160,24 @@ sub check_options {
'free' => "^node_filesystem_free.*",
'size' => "^node_filesystem_size.*",
};
foreach my $metric (@{$self->{option_results}->{metric_overload}}) {
next if ($metric !~ /(.*),(.*)/);
$self->{metrics}->{$1} = $2 if (defined($self->{metrics}->{$1}));
}
$self->{labels} = {};
foreach my $label (('instance', 'mountpoint', 'fstype')) {
if ($self->{option_results}->{$label} !~ /^(\w+)[!~=]+\".*\"$/) {
$self->{output}->add_option_msg(short_msg => "Need to specify --" . $label . " option as a PromQL filter.");
$self->{output}->option_exit();
}
$self->{labels}->{$label} = $1;
}
$self->{extra_filter} = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$self->{extra_filter} .= ',' . $filter;
}
$instance_mode = $self;
}
@ -174,25 +187,22 @@ sub manage_selection {
$self->{nodes} = {};
my $extra_filter = '';
foreach my $filter (@{$self->{option_results}->{extra_filter}}) {
$extra_filter .= ',' . $filter;
}
my $results = $options{custom}->query(queries => [ 'label_replace({__name__=~"' . $self->{metrics}->{free} . '",' .
$self->{option_results}->{instance} . ',' .
$self->{option_results}->{mountpoint} . ',' .
$self->{option_results}->{fstype} .
$self->{extra_filter} . '}, "__name__", "free", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{size} . '",' .
$self->{option_results}->{instance} . ',' .
$self->{option_results}->{mountpoint} . ',' .
$self->{option_results}->{fstype} .
$self->{extra_filter} . '}, "__name__", "size", "", "")' ]);
my $results = $options{custom}->query(queries => [ 'label_replace({__name__=~"' . $self->{metrics}->{free} . '",instance=~"' . $self->{option_results}->{node} .
'",mountpoint=~"' . $self->{option_results}->{storage} .
'",fstype!~"' . $self->{option_results}->{exclude_storage_type} .
'"' . $extra_filter . '}, "__name__", "free", "", "")',
'label_replace({__name__=~"' . $self->{metrics}->{size} . '",instance=~"' . $self->{option_results}->{node} .
'",mountpoint=~"' . $self->{option_results}->{storage} .
'",fstype!~"' . $self->{option_results}->{exclude_storage_type} .
'"' . $extra_filter . '}, "__name__", "size", "", "")' ]);
foreach my $metric (@{$results}) {
$self->{nodes}->{$metric->{metric}->{instance}}->{display} = $metric->{metric}->{instance};
$self->{nodes}->{$metric->{metric}->{instance}}->{storage}->{$metric->{metric}->{mountpoint}}->{multi} = $metric->{metric}->{instance};
$self->{nodes}->{$metric->{metric}->{instance}}->{storage}->{$metric->{metric}->{mountpoint}}->{display} = $metric->{metric}->{mountpoint};
$self->{nodes}->{$metric->{metric}->{instance}}->{storage}->{$metric->{metric}->{mountpoint}}->{$metric->{metric}->{__name__}} = ${$metric->{value}}[1];
foreach my $result (@{$results}) {
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{display} = $result->{metric}->{$self->{labels}->{instance}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{storage}->{$result->{metric}->{mountpoint}}->{multi} = $result->{metric}->{$self->{labels}->{instance}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{storage}->{$result->{metric}->{mountpoint}}->{display} = $result->{metric}->{$self->{labels}->{mountpoint}};
$self->{nodes}->{$result->{metric}->{$self->{labels}->{instance}}}->{storage}->{$result->{metric}->{mountpoint}}->{$result->{metric}->{__name__}} = ${$result->{value}}[1];
}
if (scalar(keys %{$self->{nodes}}) <= 0) {
@ -207,21 +217,21 @@ __END__
=head1 MODE
Check CPU usage for nodes and each of their cores.
Check storages usage.
=over 8
=item B<--node>
=item B<--instance>
Filter on a specific node (Must be a regexp, Default: '.*')
Filter on a specific instance (Must be a PromQL filter, Default: 'instance=~".*"')
=item B<--storage>
=item B<--mountpoint>
Filter on a specific storage (Must be a regexp, Default: '.*')
Filter on a specific mountpoint (Must be a PromQL filter, Default: 'mountpoint=~".*"')
=item B<--exclude-storage-type>
=item B<--fstype>
Exclude storage types (Must be a regexp, Default: 'rootfs|tmpfs')
Filter on a specific fstype (Must be a PromQL filter, Default: 'fstype=~"overlay"')
=item B<--units>