Skip to content

Commit 8c156cc

Browse files
authored
Merge branch 'main' into fix_broken_test
2 parents 833a71d + 3644a1e commit 8c156cc

File tree

6 files changed

+249
-7
lines changed

6 files changed

+249
-7
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: OSSF Scorecard
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
schedule:
8+
- cron: "16 11 * * 4" # once a week
9+
workflow_dispatch:
10+
11+
permissions: read-all
12+
13+
jobs:
14+
analysis:
15+
runs-on: ubuntu-latest
16+
permissions:
17+
# Needed for Code scanning upload
18+
security-events: write
19+
# Needed for GitHub OIDC token if publish_results is true
20+
id-token: write
21+
steps:
22+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
23+
with:
24+
persist-credentials: false
25+
26+
- uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
27+
with:
28+
results_file: results.sarif
29+
results_format: sarif
30+
publish_results: true
31+
32+
# Upload the results as artifacts (optional). Commenting out will disable
33+
# uploads of run results in SARIF format to the repository Actions tab.
34+
# https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts
35+
- name: "Upload artifact"
36+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
37+
with:
38+
name: SARIF file
39+
path: results.sarif
40+
retention-days: 5
41+
42+
# Upload the results to GitHub's code scanning dashboard (optional).
43+
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
44+
- name: "Upload to code-scanning"
45+
uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
46+
with:
47+
sarif_file: results.sarif

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
([#4494](https://github.com/open-telemetry/opentelemetry-python/pull/4494))
1818
- Improve CI by cancelling stale runs and setting timeouts
1919
([#4498](https://github.com/open-telemetry/opentelemetry-python/pull/4498))
20+
- Patch logging.basicConfig so OTel logs don't cause console logs to disappear
21+
([#4436](https://github.com/open-telemetry/opentelemetry-python/pull/4436))
22+
- Fix ExplicitBucketHistogramAggregation to handle multiple explicit bucket boundaries advisories
23+
([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521))
2024

2125
## Version 1.31.0/0.52b0 (2025-03-12)
2226

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,33 @@ def _init_logging(
253253
set_event_logger_provider(event_logger_provider)
254254

255255
if setup_logging_handler:
256+
_patch_basic_config()
257+
258+
# Add OTel handler
256259
handler = LoggingHandler(
257260
level=logging.NOTSET, logger_provider=provider
258261
)
259262
logging.getLogger().addHandler(handler)
260263

261264

265+
def _patch_basic_config():
266+
original_basic_config = logging.basicConfig
267+
268+
def patched_basic_config(*args, **kwargs):
269+
root = logging.getLogger()
270+
has_only_otel = len(root.handlers) == 1 and isinstance(
271+
root.handlers[0], LoggingHandler
272+
)
273+
if has_only_otel:
274+
otel_handler = root.handlers.pop()
275+
original_basic_config(*args, **kwargs)
276+
root.addHandler(otel_handler)
277+
else:
278+
original_basic_config(*args, **kwargs)
279+
280+
logging.basicConfig = patched_basic_config
281+
282+
262283
def _import_exporters(
263284
trace_exporter_names: Sequence[str],
264285
metric_exporter_names: Sequence[str],

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,18 +1387,17 @@ def _create_aggregation(
13871387
AggregationTemporality.CUMULATIVE
13881388
)
13891389

1390-
if self._boundaries is None:
1391-
self._boundaries = (
1392-
instrument._advisory.explicit_bucket_boundaries
1393-
or _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES
1394-
)
1390+
if self._boundaries is not None:
1391+
boundaries = self._boundaries
1392+
else:
1393+
boundaries = instrument._advisory.explicit_bucket_boundaries
13951394

13961395
return _ExplicitBucketHistogramAggregation(
13971396
attributes,
13981397
instrument_aggregation_temporality,
13991398
start_time_unix_nano,
14001399
reservoir_factory(_ExplicitBucketHistogramAggregation),
1401-
self._boundaries,
1400+
boundaries,
14021401
self._record_min_max,
14031402
)
14041403

opentelemetry-sdk/tests/metrics/integration_test/test_histogram_advisory_explicit_buckets.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
from unittest import TestCase
1616

1717
from opentelemetry.sdk.metrics import MeterProvider
18+
from opentelemetry.sdk.metrics._internal.aggregation import (
19+
_DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES,
20+
)
1821
from opentelemetry.sdk.metrics._internal.instrument import Histogram
1922
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
2023
from opentelemetry.sdk.metrics.view import (
@@ -164,3 +167,78 @@ def test_explicit_aggregation(self):
164167
self.assertEqual(
165168
metric.data.data_points[0].explicit_bounds, (1.0, 2.0, 3.0)
166169
)
170+
171+
def test_explicit_aggregation_multiple_histograms(self):
172+
reader = InMemoryMetricReader(
173+
preferred_aggregation={
174+
Histogram: ExplicitBucketHistogramAggregation()
175+
}
176+
)
177+
meter_provider = MeterProvider(
178+
metric_readers=[reader],
179+
)
180+
meter = meter_provider.get_meter("testmeter")
181+
182+
histogram1 = meter.create_histogram(
183+
"testhistogram1",
184+
explicit_bucket_boundaries_advisory=[1.0, 2.0, 3.0],
185+
)
186+
histogram1.record(1, {"label": "value"})
187+
histogram1.record(2, {"label": "value"})
188+
histogram1.record(3, {"label": "value"})
189+
190+
histogram2 = meter.create_histogram(
191+
"testhistogram2",
192+
explicit_bucket_boundaries_advisory=[4.0, 5.0, 6.0],
193+
)
194+
histogram2.record(4, {"label": "value"})
195+
histogram2.record(5, {"label": "value"})
196+
histogram2.record(6, {"label": "value"})
197+
198+
metrics = reader.get_metrics_data()
199+
self.assertEqual(len(metrics.resource_metrics), 1)
200+
self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1)
201+
self.assertEqual(
202+
len(metrics.resource_metrics[0].scope_metrics[0].metrics), 2
203+
)
204+
metric1 = metrics.resource_metrics[0].scope_metrics[0].metrics[0]
205+
self.assertEqual(metric1.name, "testhistogram1")
206+
self.assertEqual(
207+
metric1.data.data_points[0].explicit_bounds, (1.0, 2.0, 3.0)
208+
)
209+
metric2 = metrics.resource_metrics[0].scope_metrics[0].metrics[1]
210+
self.assertEqual(metric2.name, "testhistogram2")
211+
self.assertEqual(
212+
metric2.data.data_points[0].explicit_bounds, (4.0, 5.0, 6.0)
213+
)
214+
215+
def test_explicit_aggregation_default_boundaries(self):
216+
reader = InMemoryMetricReader(
217+
preferred_aggregation={
218+
Histogram: ExplicitBucketHistogramAggregation()
219+
}
220+
)
221+
meter_provider = MeterProvider(
222+
metric_readers=[reader],
223+
)
224+
meter = meter_provider.get_meter("testmeter")
225+
226+
histogram = meter.create_histogram(
227+
"testhistogram",
228+
)
229+
histogram.record(1, {"label": "value"})
230+
histogram.record(2, {"label": "value"})
231+
histogram.record(3, {"label": "value"})
232+
233+
metrics = reader.get_metrics_data()
234+
self.assertEqual(len(metrics.resource_metrics), 1)
235+
self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1)
236+
self.assertEqual(
237+
len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1
238+
)
239+
metric = metrics.resource_metrics[0].scope_metrics[0].metrics[0]
240+
self.assertEqual(metric.name, "testhistogram")
241+
self.assertEqual(
242+
metric.data.data_points[0].explicit_bounds,
243+
_DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES,
244+
)

opentelemetry-sdk/tests/test_configurator.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# pylint: skip-file
1616
from __future__ import annotations
1717

18+
import logging
1819
from logging import WARNING, getLogger
1920
from os import environ
2021
from typing import Iterable, Optional, Sequence
@@ -44,6 +45,7 @@
4445
_OTelSDKConfigurator,
4546
)
4647
from opentelemetry.sdk._logs import LoggingHandler
48+
from opentelemetry.sdk._logs._internal.export import LogExporter
4749
from opentelemetry.sdk._logs.export import ConsoleLogExporter
4850
from opentelemetry.sdk.environment_variables import (
4951
OTEL_TRACES_SAMPLER,
@@ -203,7 +205,7 @@ class OTLPSpanExporter:
203205
pass
204206

205207

206-
class DummyOTLPLogExporter:
208+
class DummyOTLPLogExporter(LogExporter):
207209
def __init__(self, *args, **kwargs):
208210
self.export_called = False
209211

@@ -841,6 +843,60 @@ def test_initialize_components_kwargs(
841843
True,
842844
)
843845

846+
def test_basicConfig_works_with_otel_handler(self):
847+
with ClearLoggingHandlers():
848+
_init_logging(
849+
{"otlp": DummyOTLPLogExporter},
850+
Resource.create({}),
851+
setup_logging_handler=True,
852+
)
853+
854+
logging.basicConfig(level=logging.INFO)
855+
856+
root_logger = logging.getLogger()
857+
stream_handlers = [
858+
h
859+
for h in root_logger.handlers
860+
if isinstance(h, logging.StreamHandler)
861+
]
862+
self.assertEqual(
863+
len(stream_handlers),
864+
1,
865+
"basicConfig should add a StreamHandler even when OTel handler exists",
866+
)
867+
868+
def test_basicConfig_preserves_otel_handler(self):
869+
with ClearLoggingHandlers():
870+
_init_logging(
871+
{"otlp": DummyOTLPLogExporter},
872+
Resource.create({}),
873+
setup_logging_handler=True,
874+
)
875+
876+
root_logger = logging.getLogger()
877+
self.assertEqual(
878+
len(root_logger.handlers),
879+
1,
880+
"Should be exactly one OpenTelemetry LoggingHandler",
881+
)
882+
handler = root_logger.handlers[0]
883+
self.assertIsInstance(handler, LoggingHandler)
884+
885+
logging.basicConfig()
886+
887+
self.assertGreater(len(root_logger.handlers), 1)
888+
889+
logging_handlers = [
890+
h
891+
for h in root_logger.handlers
892+
if isinstance(h, LoggingHandler)
893+
]
894+
self.assertEqual(
895+
len(logging_handlers),
896+
1,
897+
"Should still have exactly one OpenTelemetry LoggingHandler",
898+
)
899+
844900

845901
class TestMetricsInit(TestCase):
846902
def setUp(self):
@@ -1076,3 +1132,40 @@ def test_custom_configurator(self, mock_init_comp):
10761132
"sampler": "TEST_SAMPLER",
10771133
}
10781134
mock_init_comp.assert_called_once_with(**kwargs)
1135+
1136+
1137+
class ClearLoggingHandlers:
1138+
def __init__(self):
1139+
self.root_logger = getLogger()
1140+
self.original_handlers = None
1141+
1142+
def __enter__(self):
1143+
self.original_handlers = self.root_logger.handlers[:]
1144+
self.root_logger.handlers = []
1145+
return self
1146+
1147+
def __exit__(self, exc_type, exc_val, exc_tb):
1148+
self.root_logger.handlers = []
1149+
for handler in self.original_handlers:
1150+
self.root_logger.addHandler(handler)
1151+
1152+
1153+
class TestClearLoggingHandlers(TestCase):
1154+
def test_preserves_handlers(self):
1155+
root_logger = getLogger()
1156+
initial_handlers = root_logger.handlers[:]
1157+
1158+
test_handler = logging.StreamHandler()
1159+
root_logger.addHandler(test_handler)
1160+
expected_handlers = initial_handlers + [test_handler]
1161+
1162+
with ClearLoggingHandlers():
1163+
self.assertEqual(len(root_logger.handlers), 0)
1164+
temp_handler = logging.StreamHandler()
1165+
root_logger.addHandler(temp_handler)
1166+
1167+
self.assertEqual(len(root_logger.handlers), len(expected_handlers))
1168+
for h1, h2 in zip(root_logger.handlers, expected_handlers):
1169+
self.assertIs(h1, h2)
1170+
1171+
root_logger.removeHandler(test_handler)

0 commit comments

Comments
 (0)