Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
40df300
Added dropped_attributes_count to the exporter
nstawski Jun 5, 2023
e193119
Test for dropped_attributes_count
nstawski Jun 7, 2023
4b072b0
Added check for dropped_attributes
nstawski Jun 10, 2023
7e602cf
Updated to_json test
nstawski Jun 10, 2023
2461984
Lint
nstawski Jun 10, 2023
c994f3f
Lint
nstawski Jun 10, 2023
6d56b27
Fixed test
nstawski Jun 12, 2023
b017abb
Lint
nstawski Jun 12, 2023
118ca12
Lint
nstawski Jun 12, 2023
3228f14
Lint
nstawski Jun 12, 2023
3baca1f
Lint
nstawski Jun 12, 2023
2dca33b
Lint
nstawski Jun 12, 2023
5520807
Lint
nstawski Jun 13, 2023
9385b5a
Lint
nstawski Jun 13, 2023
4a833df
lint
nstawski Jun 13, 2023
e2c5c48
Lint
nstawski Jun 15, 2023
5a36085
pylint:disable=no-member for ExportLogServiceRequest in test_log_encoder
nstawski Jun 15, 2023
4ceef97
Lint
nstawski Jun 15, 2023
ec20a7d
Changelog
nstawski Jun 15, 2023
34abe7f
Merge branch 'main' into ns-3201-dropped-attributes-count-in-exporters
nstawski Jun 15, 2023
c157d93
Merge branch 'main' into ns-3201-dropped-attributes-count-in-exporters
nstawski Jun 15, 2023
0f1eeb4
Addressed pull request comments
nstawski Jun 16, 2023
64cfb27
Init
nstawski Sep 29, 2023
b1e5c47
Merge branch 'main' into ns-3201-dropped-attributes-count-in-exporters
nstawski Oct 9, 2023
afbf691
Moved the cardinality limit check out of the aggr_key check
nstawski Oct 12, 2023
b8de23b
Added tests
nstawski Oct 17, 2023
b1a76d1
Using cardinality limit
nstawski Oct 18, 2023
c731efb
Added tests for InMemoryMetricExporter
nstawski Oct 18, 2023
3682c3f
Added aggregation_cardinality_limit on view
nstawski Oct 25, 2023
b273ffc
Removed the default cardinality limit on MeterProvider
nstawski Oct 25, 2023
456fd2e
Changelog update
nstawski Oct 26, 2023
b026f10
Merge branch 'main' into ns-3201-dropped-attributes-count-in-exporters
nstawski Oct 26, 2023
f442889
Merge branch 'main' into ns-3201-dropped-attributes-count-in-exporters
nstawski Oct 8, 2024
2ecde0f
Update opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metr…
nstawski Oct 8, 2024
8b74c76
Code cleanup, removed the InMemoryMetricExporter, updated tests
nstawski Oct 8, 2024
812905b
Code cleanup
nstawski Oct 8, 2024
f382f7d
Code cleanup
nstawski Oct 8, 2024
c647a60
Code cleanup
nstawski Oct 9, 2024
38a3677
Code cleanup
nstawski Oct 9, 2024
b5daf85
Catch the warning for the failing test
nstawski Oct 9, 2024
171087f
Tests updated
nstawski Oct 9, 2024
960e170
Tests updated
nstawski Oct 9, 2024
c50370f
Tests updated
nstawski Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/generate_workflows.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from pathlib import Path

from generate_workflows_lib import (
generate_test_workflow,
generate_lint_workflow,
generate_contrib_workflow,
generate_misc_workflow
generate_lint_workflow,
generate_misc_workflow,
generate_test_workflow,
)

tox_ini_path = Path(__file__).parent.parent.parent.joinpath("tox.ini")
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4094](https://github.com/open-telemetry/opentelemetry-python/pull/4094))
- Implement events sdk
([#4176](https://github.com/open-telemetry/opentelemetry-python/pull/4176))
- Basic protection against the unintended cardinality explosion
([#3486](https://github.com/open-telemetry/opentelemetry-python/pull/3486))

## Version 1.27.0/0.48b0 (2024-08-28)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from opentelemetry.sdk.metrics._internal.view import View

_logger = getLogger(__name__)
_OVERFLOW_ATTRIBUTE = ("otel.metric.overflow", "true")
_DEFAULT_AGGREGATION_LIMIT = 2000


class _ViewInstrumentMatch:
Expand All @@ -45,6 +47,9 @@ def __init__(
self._attributes_aggregation: Dict[frozenset, _Aggregation] = {}
self._lock = Lock()
self._instrument_class_aggregation = instrument_class_aggregation
self._aggregation_cardinality_limit = (
view._aggregation_cardinality_limit or _DEFAULT_AGGREGATION_LIMIT
)
self._name = self._view._name or self._instrument.name
self._description = (
self._view._description or self._instrument.description
Expand Down Expand Up @@ -87,6 +92,20 @@ def conflicts(self, other: "_ViewInstrumentMatch") -> bool:

return result

def get_aggregation_key(self, attributes):
if (
len(self._attributes_aggregation)
>= self._aggregation_cardinality_limit - 1
):
_logger.warning(
"Metric cardinality limit of %s exceeded. Aggregating under overflow attribute.",
self._aggregation_cardinality_limit,
)
aggr_key = frozenset([_OVERFLOW_ATTRIBUTE])
else:
aggr_key = frozenset(attributes.items())
return aggr_key

# pylint: disable=protected-access
def consume_measurement(
self, measurement: Measurement, should_sample_exemplar: bool = True
Expand All @@ -104,7 +123,7 @@ def consume_measurement(
else:
attributes = {}

aggr_key = frozenset(attributes.items())
aggr_key = self.get_aggregation_key(attributes)

if aggr_key not in self._attributes_aggregation:
with self._lock:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def __init__(
sdk_config: SdkConfiguration,
instrument_class_temporality: Dict[type, AggregationTemporality],
instrument_class_aggregation: Dict[type, Aggregation],
aggregation_cardinality_limit: Optional[int] = None,
) -> None:
self._lock = RLock()
self._sdk_config = sdk_config
Expand All @@ -74,6 +75,7 @@ def __init__(
] = {}
self._instrument_class_temporality = instrument_class_temporality
self._instrument_class_aggregation = instrument_class_aggregation
self._aggregation_cardinality_limit = aggregation_cardinality_limit

def _get_or_init_view_instrument_match(
self, instrument: Instrument
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def __init__(
Callable[[Type[_Aggregation]], ExemplarReservoirBuilder]
] = None,
instrument_unit: Optional[str] = None,
aggregation_cardinality_limit: Optional[int] = None,
Copy link
Member

Choose a reason for hiding this comment

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

Looks like these args are individually documented in the docstring above. Should we add one for this arg?

):
if (
instrument_type
Expand Down Expand Up @@ -158,6 +159,7 @@ def __init__(
self._description = description
self._attribute_keys = attribute_keys
self._aggregation = aggregation or self._default_aggregation
self._aggregation_cardinality_limit = aggregation_cardinality_limit
self._exemplar_reservoir_factory = (
exemplar_reservoir_factory or _default_reservoir_factory
)
Expand Down
46 changes: 37 additions & 9 deletions opentelemetry-sdk/tests/logs/test_log_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,18 @@ def test_log_record_dropped_attributes_set_limits_max_attribute(self):
max_attributes=1,
)

result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)
with warnings.catch_warnings(record=True) as cw:
warnings.simplefilter("always")
result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)

self.assertTrue(
any(
issubclass(warning.category, LogDroppedAttributesWarning)
for warning in cw
)
)
self.assertTrue(result.dropped_attributes == 1)

def test_log_record_dropped_attributes_set_limits_max_attribute_length(
Expand All @@ -93,9 +102,18 @@ def test_log_record_dropped_attributes_set_limits_max_attribute_length(
max_attribute_length=1,
)

result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)
with warnings.catch_warnings(record=True) as cw:
warnings.simplefilter("always")
result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)
# would not be dropping any attributes if the limit is not set
self.assertFalse(
any(
issubclass(warning.category, LogDroppedAttributesWarning)
for warning in cw
)
)
self.assertTrue(result.dropped_attributes == 0)
self.assertEqual(expected, result.attributes)

Expand All @@ -107,9 +125,19 @@ def test_log_record_dropped_attributes_set_limits(self):
max_attribute_length=1,
)

result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)
with warnings.catch_warnings(record=True) as cw:
warnings.simplefilter("always")
result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)

self.assertTrue(
any(
issubclass(warning.category, LogDroppedAttributesWarning)
for warning in cw
)
)

self.assertTrue(result.dropped_attributes == 1)
self.assertEqual(expected, result.attributes)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from logging import WARNING
from unittest import TestCase

from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader


class TestMetricCardinalityLimit(TestCase):

def setUp(self):
self.reader = InMemoryMetricReader()
self.meter_provider = MeterProvider(metric_readers=[self.reader])
self.meter = self.meter_provider.get_meter("test_meter")

def test_metric_cardinality_limit(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

If we are introducing the ability to configure the value we should have a test for that too.

# Assuming a Counter type metric
counter = self.meter.create_counter("cardinality_test_counter")

# Generate and add more than 2000 unique labels
for ind in range(2100):
label = {"key": f"value_{ind}"}
counter.add(1, label)

# Simulate an export to get the metrics into the in-memory exporter
self.reader.force_flush()

# Retrieve the metrics from the in-memory exporter
metric_data = self.reader.get_metrics_data()

# Check if the length of the metric data doesn't exceed 2000
self.assertTrue(len(metric_data.resource_metrics) <= 2000)

# Check if a warning or an error was logged
with self.assertLogs(level=WARNING):
counter.add(1, {"key": "value_2101"})
5 changes: 3 additions & 2 deletions scripts/add_required_checks.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# This script is to be used by maintainers by running it locally.

from requests import put
from json import dumps
from os import environ

from requests import put
from yaml import safe_load
from json import dumps

job_names = ["EasyCLA"]

Expand Down
3 changes: 2 additions & 1 deletion scripts/eachdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import shutil
import subprocess
import sys
from toml import load
from configparser import ConfigParser
from inspect import cleandoc
from itertools import chain
from os.path import basename
from pathlib import Path, PurePath

from toml import load

DEFAULT_ALLSEP = " "
DEFAULT_ALLFMT = "{rel}"

Expand Down