Skip to content

Commit 56495ed

Browse files
authored
Add schema_url to Resource (#1871)
1 parent c8ebda9 commit 56495ed

File tree

4 files changed

+189
-10
lines changed

4 files changed

+189
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
([#1855](https://github.com/open-telemetry/opentelemetry-python/pull/1855))
1414
- Fixed exporter OTLP header parsing to match baggage header formatting.
1515
([#1869](https://github.com/open-telemetry/opentelemetry-python/pull/1869))
16+
- Added optional `schema_url` field to `Resource` class
17+
([#1871](https://github.com/open-telemetry/opentelemetry-python/pull/1871))
1618

1719
## [1.2.0, 0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11
1820

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

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,25 @@
141141
class Resource:
142142
"""A Resource is an immutable representation of the entity producing telemetry as Attributes."""
143143

144-
def __init__(self, attributes: Attributes):
144+
def __init__(
145+
self, attributes: Attributes, schema_url: typing.Optional[str] = None
146+
):
145147
_filter_attributes(attributes)
146148
self._attributes = attributes.copy()
149+
if schema_url is None:
150+
schema_url = ""
151+
self._schema_url = schema_url
147152

148153
@staticmethod
149-
def create(attributes: typing.Optional[Attributes] = None) -> "Resource":
154+
def create(
155+
attributes: typing.Optional[Attributes] = None,
156+
schema_url: typing.Optional[str] = None,
157+
) -> "Resource":
150158
"""Creates a new `Resource` from attributes.
151159
152160
Args:
153161
attributes: Optional zero or more key-value pairs.
162+
schema_url: Optional URL pointing to the schema
154163
155164
Returns:
156165
The newly-created Resource.
@@ -159,7 +168,7 @@ def create(attributes: typing.Optional[Attributes] = None) -> "Resource":
159168
attributes = {}
160169
resource = _DEFAULT_RESOURCE.merge(
161170
OTELResourceDetector().detect()
162-
).merge(Resource(attributes))
171+
).merge(Resource(attributes, schema_url))
163172
if not resource.attributes.get(SERVICE_NAME, None):
164173
default_service_name = "unknown_service"
165174
process_executable_name = resource.attributes.get(
@@ -168,7 +177,7 @@ def create(attributes: typing.Optional[Attributes] = None) -> "Resource":
168177
if process_executable_name:
169178
default_service_name += ":" + process_executable_name
170179
resource = resource.merge(
171-
Resource({SERVICE_NAME: default_service_name})
180+
Resource({SERVICE_NAME: default_service_name}, schema_url)
172181
)
173182
return resource
174183

@@ -180,12 +189,21 @@ def get_empty() -> "Resource":
180189
def attributes(self) -> Attributes:
181190
return self._attributes.copy()
182191

192+
@property
193+
def schema_url(self) -> str:
194+
return self._schema_url
195+
183196
def merge(self, other: "Resource") -> "Resource":
184197
"""Merges this resource and an updating resource into a new `Resource`.
185198
186199
If a key exists on both the old and updating resource, the value of the
187200
updating resource will override the old resource value.
188201
202+
The updating resource's `schema_url` will be used only if the old
203+
`schema_url` is empty. Attempting to merge two resources with
204+
different, non-empty values for `schema_url` will result in an error
205+
and return the old resource.
206+
189207
Args:
190208
other: The other resource to be merged.
191209
@@ -194,15 +212,35 @@ def merge(self, other: "Resource") -> "Resource":
194212
"""
195213
merged_attributes = self.attributes
196214
merged_attributes.update(other.attributes)
197-
return Resource(merged_attributes)
215+
216+
if self.schema_url == "":
217+
schema_url = other.schema_url
218+
elif other.schema_url == "":
219+
schema_url = self.schema_url
220+
elif self.schema_url == other.schema_url:
221+
schema_url = other.schema_url
222+
else:
223+
logger.error(
224+
"Failed to merge resources: The two schemas %s and %s are incompatible",
225+
self.schema_url,
226+
other.schema_url,
227+
)
228+
return self
229+
230+
return Resource(merged_attributes, schema_url)
198231

199232
def __eq__(self, other: object) -> bool:
200233
if not isinstance(other, Resource):
201234
return False
202-
return self._attributes == other._attributes
235+
return (
236+
self._attributes == other._attributes
237+
and self._schema_url == other._schema_url
238+
)
203239

204240
def __hash__(self):
205-
return hash(dumps(self._attributes, sort_keys=True))
241+
return hash(
242+
f"{dumps(self._attributes, sort_keys=True)}|{self._schema_url}"
243+
)
206244

207245

208246
_EMPTY_RESOURCE = Resource({})

opentelemetry-sdk/tests/resources/test_resources.py

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import os
1818
import unittest
1919
import uuid
20+
from logging import ERROR
2021
from unittest import mock
2122

2223
from opentelemetry.sdk import resources
@@ -51,6 +52,14 @@ def test_create(self):
5152
resource = resources.Resource.create(attributes)
5253
self.assertIsInstance(resource, resources.Resource)
5354
self.assertEqual(resource.attributes, expected_attributes)
55+
self.assertEqual(resource.schema_url, "")
56+
57+
schema_url = "https://opentelemetry.io/schemas/1.3.0"
58+
59+
resource = resources.Resource.create(attributes, schema_url)
60+
self.assertIsInstance(resource, resources.Resource)
61+
self.assertEqual(resource.attributes, expected_attributes)
62+
self.assertEqual(resource.schema_url, schema_url)
5463

5564
os.environ[resources.OTEL_RESOURCE_ATTRIBUTES] = "key=value"
5665
resource = resources.Resource.create(attributes)
@@ -67,17 +76,45 @@ def test_create(self):
6776
self.assertEqual(
6877
resource,
6978
resources._DEFAULT_RESOURCE.merge(
70-
resources.Resource({resources.SERVICE_NAME: "unknown_service"})
79+
resources.Resource(
80+
{resources.SERVICE_NAME: "unknown_service"}, ""
81+
)
7182
),
7283
)
84+
self.assertEqual(resource.schema_url, "")
85+
86+
resource = resources.Resource.create(None, None)
87+
self.assertEqual(
88+
resource,
89+
resources._DEFAULT_RESOURCE.merge(
90+
resources.Resource(
91+
{resources.SERVICE_NAME: "unknown_service"}, ""
92+
)
93+
),
94+
)
95+
self.assertEqual(resource.schema_url, "")
7396

7497
resource = resources.Resource.create({})
7598
self.assertEqual(
7699
resource,
77100
resources._DEFAULT_RESOURCE.merge(
78-
resources.Resource({resources.SERVICE_NAME: "unknown_service"})
101+
resources.Resource(
102+
{resources.SERVICE_NAME: "unknown_service"}, ""
103+
)
104+
),
105+
)
106+
self.assertEqual(resource.schema_url, "")
107+
108+
resource = resources.Resource.create({}, None)
109+
self.assertEqual(
110+
resource,
111+
resources._DEFAULT_RESOURCE.merge(
112+
resources.Resource(
113+
{resources.SERVICE_NAME: "unknown_service"}, ""
114+
)
79115
),
80116
)
117+
self.assertEqual(resource.schema_url, "")
81118

82119
def test_resource_merge(self):
83120
left = resources.Resource({"service": "ui"})
@@ -86,6 +123,33 @@ def test_resource_merge(self):
86123
left.merge(right),
87124
resources.Resource({"service": "ui", "host": "service-host"}),
88125
)
126+
schema_urls = (
127+
"https://opentelemetry.io/schemas/1.2.0",
128+
"https://opentelemetry.io/schemas/1.3.0",
129+
)
130+
131+
left = resources.Resource.create({}, None)
132+
right = resources.Resource.create({}, None)
133+
self.assertEqual(left.merge(right).schema_url, "")
134+
135+
left = resources.Resource.create({}, None)
136+
right = resources.Resource.create({}, schema_urls[0])
137+
self.assertEqual(left.merge(right).schema_url, schema_urls[0])
138+
139+
left = resources.Resource.create({}, schema_urls[0])
140+
right = resources.Resource.create({}, None)
141+
self.assertEqual(left.merge(right).schema_url, schema_urls[0])
142+
143+
left = resources.Resource.create({}, schema_urls[0])
144+
right = resources.Resource.create({}, schema_urls[0])
145+
self.assertEqual(left.merge(right).schema_url, schema_urls[0])
146+
147+
left = resources.Resource.create({}, schema_urls[0])
148+
right = resources.Resource.create({}, schema_urls[1])
149+
with self.assertLogs(level=ERROR) as log_entry:
150+
self.assertEqual(left.merge(right), left)
151+
self.assertIn(schema_urls[0], log_entry.output[0])
152+
self.assertIn(schema_urls[1], log_entry.output[0])
89153

90154
def test_resource_merge_empty_string(self):
91155
"""Verify Resource.merge behavior with the empty string.
@@ -130,6 +194,11 @@ def test_immutability(self):
130194
attributes["cost"] = 999.91
131195
self.assertEqual(resource.attributes, attributes_copy)
132196

197+
with self.assertRaises(AttributeError):
198+
resource.schema_url = "bug"
199+
200+
self.assertEqual(resource.schema_url, "")
201+
133202
def test_service_name_using_process_name(self):
134203
resource = resources.Resource.create(
135204
{resources.PROCESS_EXECUTABLE_NAME: "test"}
@@ -220,6 +289,76 @@ def test_aggregated_resources_multiple_detectors(self):
220289
),
221290
)
222291

292+
def test_aggregated_resources_different_schema_urls(self):
293+
resource_detector1 = mock.Mock(spec=resources.ResourceDetector)
294+
resource_detector1.detect.return_value = resources.Resource(
295+
{"key1": "value1"}, ""
296+
)
297+
resource_detector2 = mock.Mock(spec=resources.ResourceDetector)
298+
resource_detector2.detect.return_value = resources.Resource(
299+
{"key2": "value2", "key3": "value3"}, "url1"
300+
)
301+
resource_detector3 = mock.Mock(spec=resources.ResourceDetector)
302+
resource_detector3.detect.return_value = resources.Resource(
303+
{
304+
"key2": "try_to_overwrite_existing_value",
305+
"key3": "try_to_overwrite_existing_value",
306+
"key4": "value4",
307+
},
308+
"url2",
309+
)
310+
resource_detector4 = mock.Mock(spec=resources.ResourceDetector)
311+
resource_detector4.detect.return_value = resources.Resource(
312+
{
313+
"key2": "try_to_overwrite_existing_value",
314+
"key3": "try_to_overwrite_existing_value",
315+
"key4": "value4",
316+
},
317+
"url1",
318+
)
319+
self.assertEqual(
320+
resources.get_aggregated_resources(
321+
[resource_detector1, resource_detector2]
322+
),
323+
resources.Resource(
324+
{"key1": "value1", "key2": "value2", "key3": "value3"},
325+
"url1",
326+
),
327+
)
328+
with self.assertLogs(level=ERROR) as log_entry:
329+
self.assertEqual(
330+
resources.get_aggregated_resources(
331+
[resource_detector2, resource_detector3]
332+
),
333+
resources.Resource(
334+
{"key2": "value2", "key3": "value3"}, "url1"
335+
),
336+
)
337+
self.assertIn("url1", log_entry.output[0])
338+
self.assertIn("url2", log_entry.output[0])
339+
with self.assertLogs(level=ERROR):
340+
self.assertEqual(
341+
resources.get_aggregated_resources(
342+
[
343+
resource_detector2,
344+
resource_detector3,
345+
resource_detector4,
346+
resource_detector1,
347+
]
348+
),
349+
resources.Resource(
350+
{
351+
"key1": "value1",
352+
"key2": "try_to_overwrite_existing_value",
353+
"key3": "try_to_overwrite_existing_value",
354+
"key4": "value4",
355+
},
356+
"url1",
357+
),
358+
)
359+
self.assertIn("url1", log_entry.output[0])
360+
self.assertIn("url2", log_entry.output[0])
361+
223362
def test_resource_detector_ignore_error(self):
224363
resource_detector = mock.Mock(spec=resources.ResourceDetector)
225364
resource_detector.detect.side_effect = Exception()

opentelemetry-sdk/tests/trace/test_trace.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,7 @@ def test_span_override_start_and_end_time(self):
869869
self.assertEqual(end_time, span.end_time)
870870

871871
def test_ended_span(self):
872-
""""Events, attributes are not allowed after span is ended"""
872+
"""Events, attributes are not allowed after span is ended"""
873873

874874
root = self.tracer.start_span("root")
875875

0 commit comments

Comments
 (0)