Skip to content
This repository was archived by the owner on Sep 17, 2025. It is now read-only.

Implement Standard Metrics - Free Memory#708

Merged
reyang merged 45 commits intocensus-instrumentation:masterfrom
lzchen:metrics
Jul 13, 2019
Merged

Implement Standard Metrics - Free Memory#708
reyang merged 45 commits intocensus-instrumentation:masterfrom
lzchen:metrics

Conversation

@lzchen
Copy link
Contributor

@lzchen lzchen commented Jul 1, 2019

Initial implementation of standard metrics. [#695]
Standard Metrics are defined as metrics that will be exported by default using the metrics exporter.
Standard metrics are enabled by default as suggested by @c24t

The first one that I am including is Free Memory in bytes.
Free Memory is defined as: memory not being used at all (zeroed) that is readily available; note that this doesn’t reflect the actual memory available (use available instead). total - used does not necessarily match free. Defined in https://psutil.readthedocs.io/en/latest/

In Azure Monitor, there are special namespace names to denote a metric as being "standard". For Free Memory, you can view the telemetry in metrics explorer by going to Standard Metrics (preview) namespace and Available Memory under Performance Counters.

As for architectural decisions, the periodic recording of the standard metrics must be non-blocking to the main thread. So I chose to use a separate recording thread (initiated by MetricExporterTask) to perform this. The exporter and recorder threads should be decoupled to perform their specific jobs (the simpler the task, the better on cpu and memory).

There is a possible race condition in which the exporting is called before the recording, but the metric will then be simply delayed by one interval, which I don't think is a huge issue.

@lzchen lzchen requested review from a team, c24t, reyang and songy23 as code owners July 1, 2019 23:15
@lzchen
Copy link
Contributor Author

lzchen commented Jul 1, 2019

image

@lzchen
Copy link
Contributor Author

lzchen commented Jul 1, 2019

image

exporter.enable_standard_metrics()
# Separate thread for recording of metrics
transport.get_recorder_thread(exporter.record_standard_metrics,
interval=options_.export_interval)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if the recording interval should be the same as export interval. What are the other SDKs doing today?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Node SDK, the intervals for recording and exporting are different and can be configured. I was using the same interval because I was thinking of simplicity for the user so they do not have to configure anything in options. Perhaps derive the recording interval from the export interval (maybe interval-%age), as long as the recording happens prior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reyang We can also add another option in Options. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how we plan to do this in OpenCensus/OpenTelemetry, it is not supposed to be exporter specific I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it won't be exporter specific, we can abstract out Options to have OpenCensus default behaviour and then implement exporter specific behaviour. Just an idea.

install_requires=[
'opencensus >= 0.7.dev0, < 1.0.0',
'requests >= 2.19.0',
'psutil >= 5.6.3',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, sort

:return: A running thread responsible for executing record_metrics().

"""
tt = MetricExporterTask(interval, record_metrics)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should find another name for MetricExporterTask as we started to use it for non-exporting tasks.

:return: A running thread responsible for executing record_metrics().

"""
tt = MetricExporterTask(interval, record_metrics)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should find another name for MetricExporterTask as we started to use it for non-exporting tasks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I will use something more appropriate.

def enable_standard_metrics(self):
# Sets up stats related objects to begin
# recording standard metrics
stats_ = stats.stats
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, why not use stats_module.stats for consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll use stats_module.stats for consistency.

# Function called periodically to record standard metrics
vmem = psutil.virtual_memory()
available_memory = vmem.available
available_measure = self.standard_metrics_measure_map[StandardMetricType.AVAILABLE_MEMORY]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'available_measure' name is confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I will match with the previous naming.

# Uniqueness should be based off the name of the view and
# measure. A warning message in the logs will be shown
# if there are duplicate names
self.standard_metrics_map = stats_recorder.new_measurement_map()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider decoupling the actual logic from the exporter class. We will need to add several default metrics, need to control the size of this file and have a better organized way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I will do this.

Copy link
Member

@c24t c24t left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered using gauges for this? This is the ideal use case: instantaneous measurements that don't need to be aggregated and can be collected at export time.

If you use a derived gauge here it'll call psutil only when you collect the metrics for export:

import psutil
from opencensus.metrics.export.gauge import Registry
from opencensus.metrics.export.gauge import DerivedLongGauge

def get_available_memory():
    return psutil.virtual_memory().available

gauge = DerivedLongGauge('available_memory', 'Bytes of available memory on the host', 'byte', [])
gauge.create_default_time_series(get_available_memory)

reg = Registry()
reg.add_gauge(gauge)

[metric] = reg.get_metrics()
print(metric)
print(metric.time_series)
print(metric.time_series[0].points[0].value.value)

gives:

>>> Metric(time_series=<1 TimeSeries>, descriptor.name="available_memory")
>>> [TimeSeries([ValueLong(6316986368)], label_values=(), start_timestamp=2019-07-03 22:17:44.561518)]
>>> 6316986368

Gauges are relatively new, and you might discover they still need some work when you use them in the exporter, but since this solution doesn't rely on views at all it's more future-proof.

class StandardMetricsType(object):
# Memory that can be given instantly to
# processes without the system going into swap
AVAILABLE_MEMORY = "\\Memory\\Available Bytes"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this come from, and why the backslashes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the namespace that is used in Azure Monitor that uniquely identifies certain standard metrics.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, any reason not to move this up to the module?

# the storage only uses one single data structure
# Uniqueness should be based off the name of the view and
# measure. A warning message in the logs will be shown
# if there are duplicate names
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, here and above: what's with the short text wrapping?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to pass the lint checks for maximum characters. I just eyeballed the length.

@lzchen
Copy link
Contributor Author

lzchen commented Jul 3, 2019

@c24t The gauges look really cool! DerivedGauge seems to fit exactly what we need. We can also get rid of the recorder thread this way. If this is implemented for standard metrics, then the exporter would essentially be "getting metrics" from two different places, the gauges registry and the measure to view map. So before they are exported, these two lists should be merged. There is a case where measure_to_view_map produces metrics that have the same name as the certain metrics produces from the gauges registry. But since we are in control of the naming for standard metrics, I don't think this is a huge issue. What do you think?

@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored or co-authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of all the commit author(s), set the cla label to yes (if enabled on your project), and then merge this pull request when appropriate.

ℹ️ Googlers: Go here for more info.

@googlebot googlebot added cla: no and removed cla: yes labels Jul 5, 2019
@lzchen
Copy link
Contributor Author

lzchen commented Jul 5, 2019

Implementation now uses Gauges instead of measures and views. StandardMetricProducer and BaseStandardMetric are also abstracted into the core sdk. Developers can make their own Producers and StandardMetrics that extend from these classes.

# https://psutil.readthedocs.io/en/latest/
class AvailableMemoryStandardMetric(BaseStandardMetric):
""" Metric for Available Memory
Avaliable memoru is defined as memory that can be given
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: memoru -> memory

execution_context.set_measure_to_view_map({})
stats_module.stats = stats_module._Stats()

def test_constructor_and_setup(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentionally empty (since the purpose is to call setUp)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My mistake I should have commented this out. I tend to leave writing tests once the design changes have been approved.

Get a `PeriodicTask` that periodically calls:

exporter.export_metrics(metric_producer.get_metrics())
exporter.export_metrics(all_metrics)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider putting the actual code example here (instead of the description below).
Consider itertools.chain.



def get_exporter_thread(metric_producer, exporter, interval=None):
def get_exporter_thread(metric_producers, exporter, interval=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a design question, do we intend to have one MetricProducer taking multiple metrics, or multiple MetricProducer (which I guess is the case)? If it is the latter, what's the main consideration of having multiple producers (e.g. just to categorize by ownership, or ext lib, or we expect each producer to export individually?)

Copy link
Contributor Author

@lzchen lzchen Jul 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we intend to take multiple MetricProducer. The idea is simply as you noted to categorize ownership of the types of metrics that each Producer produces. My thought was to separate these based on logic (separate standard from custom in this case). If a developer would like to produce some other set of metrics using a different Producer, they can simply create their own MetricProducer and implement "get_metrics".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@c24t would you take a look at the changes to the core library? Thanks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change looks fine, but the other option is to add a metric producer class that calls other metric producers under the hood. Since we're likely to make a similar change for exporters we might want to revisit this later. But note that this change changes the API, the other way doesn't.

exporter.export_metrics.assert_called_once_with(metrics)
finally:
task.cancel()
task.join() No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, new line before EOF

options = Options(**options)
exporter = MetricsExporter(options=options)
transport.get_exporter_thread(stats.stats,
options_ = Options(**options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion here, but I was curious on why do we need options_ instead of using options = Options(**options).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was to differentiate the parameter from the local variable. I didn't want to name it something completely different.

'test',
'test',
[])
test_gauge.create_default_time_series(lambda x: (1))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why (1) instead of 1?

# Definitions taken from psutil docs
# https://psutil.readthedocs.io/en/latest/
class AvailableMemoryStandardMetric(BaseStandardMetric):
""" Metric for Available Memory
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor, the comments are using different styles, it'll be good to keep a consistent style.

Copy link
Member

@c24t c24t left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stepping back, I don't think there's a good case for standard_metrics.py in the core library, but the changes in the azure exporter look fine. Some comments on the code, structure, and style.

self.assertEqual(len(exporter_mock.call_args[0][0]), 2)

@mock.patch('opencensus.ext.azure.metrics_exporter' +
'.transport.get_exporter_thread', return_value=mock.Mock())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These will join without the +, and isn't returning a mock the default behavior?

def __init__(self):
super(StandardMetricsProducer, self).__init__()

"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring in the wrong place?

using gauges
"""
def __init__(self):
super(StandardMetricsProducer, self).__init__()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd call the super init anyway if this init didn't exist.


class StandardMetricsProducer(Registry):
"""Implementation of the producer of standard metrics
using gauges
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last comment about docstring line wrapping:

class StandardMetricsProducer(Registry):
    """Some short description up to 79 chars.

    Longer flowing text isn't indented, and can wrap and wrap and wrap and wrap
    and wrap and wrap and wrap and wrap and wrap.

    :type some_arg: document init args in the class docstring
    :param some_arg: :class:`OrOmitIfThereArentAny`
    """
    def __init__(self):
        ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really we ought to catch this in lint to avoid this kind of comment in review.

instantly to processes without the system going into swap
"""
def __init__(self):
super(AvailableMemoryStandardMetric, self).__init__()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This init is a no-op too.

types = [a for a in attributes
if not(a[0].startswith('__') and
a[0].endswith('__'))]
self.assertEquals(len(producer.metrics), len(types))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be a lot clearer just to list these out (especially since there's only one metric at the moment) than call inspect in the test.

export(itertools.chain(*all_gets))

:type metric_producer:
where all_gets is the concatenation of all metrics produced by
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dedent this to get it to render as text, and consider wrapping the code blocks in backticks. You might want to generate the docs to see how this looks.

metric_producer.get_metrics()

:type metric_producers: list
:param metric_producers: The list of metric producers to use to get metrics
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing, move down a line.



def get_exporter_thread(metric_producer, exporter, interval=None):
def get_exporter_thread(metric_producers, exporter, interval=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change looks fine, but the other option is to add a metric producer class that calls other metric producers under the hood. Since we're likely to make a similar change for exporters we might want to revisit this later. But note that this change changes the API, the other way doesn't.

CHANGELOG.md Outdated
# Changelog

## Unreleased
- Refactored PeriodicMetricTask
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog should generally describe user-facing changes.

@lzchen
Copy link
Contributor Author

lzchen commented Jul 10, 2019

@c24t Thanks for reviewing. My initial design idea was to abstract the metric types and producers so that developers can write their own standard metrics and producers. However I realize that Metric_producer class does essentially this for ALL metric producers; there is no need to have an interface for STANDARD metric producer.

@lzchen
Copy link
Contributor Author

lzchen commented Jul 11, 2019

@c24t I removed the standard metrics file from the core sdk as per your suggestion. It doesn't make a lot of sense to include it. I have also made changes to the docstrings to adhere to the recommended practices for python docstrings. Can you take a look when you have the time?

Copy link
Member

@c24t c24t left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only minor comments, LGTM!

md = metric.descriptor
# Each time series will be uniquely
# identified by it's label values
# Each time series will be uniquely identified by it's
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Each time series will be uniquely identified by it's
# Each time series will be uniquely identified by its


self.assertEqual(exporter.options.instrumentation_key, iKey)
self.assertEqual(len(exporter_mock.call_args_list), 1)
self.assertEqual(len(exporter_mock.call_args[0][0]), 2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not check that the the actual metric producer is in the arg list here?

print("Done recording metrics")


if __name__ == "__main__":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More for @reyang than @lzchen: if you expect users to actually run the azure example scripts you might want to make them executable and add a #!/usr/bin/env python line up top. Not for this PR, but to consider later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And chmod 755.

@lzchen
Copy link
Contributor Author

lzchen commented Jul 12, 2019

@reyang Ready to merge!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants