Skip to content

Commit 02a5a7d

Browse files
authored
Add resource attributes to the LogRecord (#26)
1 parent e902059 commit 02a5a7d

File tree

4 files changed

+82
-74
lines changed

4 files changed

+82
-74
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "partial_span_processor"
7-
version = "0.0.5"
7+
version = "0.0.6"
88
authors = [
99
{ name = "Mladjan Gadzic", email = "[email protected]" }
1010
]

src/partial_span_processor/__init__.py

Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,26 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import base64
1618
import threading
1719
import time
1820
from queue import Queue
19-
from typing import Optional
21+
from typing import TYPE_CHECKING
2022

21-
from opentelemetry import context as context_api
2223
from opentelemetry._logs.severity import SeverityNumber
2324
from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans
2425
from opentelemetry.proto.trace.v1 import trace_pb2
2526
from opentelemetry.sdk._logs import LogData, LogRecord
26-
from opentelemetry.sdk._logs.export import LogExporter
27-
from opentelemetry.sdk.trace import (
28-
SpanProcessor,
29-
Span,
30-
ReadableSpan
31-
)
27+
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
3228
from opentelemetry.trace import TraceFlags
3329

30+
if TYPE_CHECKING:
31+
from opentelemetry import context as context_api
32+
from opentelemetry.sdk._logs.export import LogExporter
33+
from opentelemetry.sdk.resources import Resource
34+
3435
WORKER_THREAD_NAME = "OtelPartialSpanProcessor"
3536

3637

@@ -39,12 +40,15 @@ class PartialSpanProcessor(SpanProcessor):
3940
def __init__(
4041
self,
4142
log_exporter: LogExporter,
42-
heartbeat_interval_millis: int
43-
):
43+
heartbeat_interval_millis: int,
44+
resource: Resource | None = None,
45+
) -> None:
4446
if heartbeat_interval_millis <= 0:
45-
raise ValueError("heartbeat_interval_ms must be greater than 0")
47+
msg = "heartbeat_interval_ms must be greater than 0"
48+
raise ValueError(msg)
4649
self.log_exporter = log_exporter
4750
self.heartbeat_interval_millis = heartbeat_interval_millis
51+
self.resource = resource
4852

4953
self.active_spans = {}
5054
self.ended_spans = Queue()
@@ -53,11 +57,11 @@ def __init__(
5357
self.done = False
5458
self.condition = threading.Condition(threading.Lock())
5559
self.worker_thread = threading.Thread(
56-
name=WORKER_THREAD_NAME, target=self.worker, daemon=True
60+
name=WORKER_THREAD_NAME, target=self.worker, daemon=True,
5761
)
5862
self.worker_thread.start()
5963

60-
def worker(self):
64+
def worker(self) -> None:
6165
while not self.done:
6266
with self.condition:
6367
self.condition.wait(self.heartbeat_interval_millis / 1000)
@@ -73,17 +77,17 @@ def worker(self):
7377

7478
self.heartbeat()
7579

76-
def heartbeat(self):
80+
def heartbeat(self) -> None:
7781
with self.lock:
78-
for span_key, span in list(self.active_spans.items()):
82+
for span in list(self.active_spans.values()):
7983
attributes = self.get_heartbeat_attributes()
80-
log_data = get_log_data(span, attributes)
84+
log_data = self.get_log_data(span, attributes)
8185
self.log_exporter.export([log_data])
8286

83-
def on_start(self, span: "Span",
84-
parent_context: Optional[context_api.Context] = None) -> None:
87+
def on_start(self, span: Span,
88+
parent_context: context_api.Context | None = None) -> None:
8589
attributes = self.get_heartbeat_attributes()
86-
log_data = get_log_data(span, attributes)
90+
log_data = self.get_log_data(span, attributes)
8791
self.log_exporter.export([log_data])
8892

8993
span_key = (span.context.trace_id, span.context.span_id)
@@ -92,7 +96,7 @@ def on_start(self, span: "Span",
9296

9397
def on_end(self, span: ReadableSpan) -> None:
9498
attributes = get_stop_attributes()
95-
log_data = get_log_data(span, attributes)
99+
log_data = self.get_log_data(span, attributes)
96100
self.log_exporter.export([log_data])
97101

98102
span_key = (span.context.trace_id, span.context.span_id)
@@ -105,39 +109,38 @@ def shutdown(self) -> None:
105109
self.condition.notify_all()
106110
self.worker_thread.join()
107111

108-
def get_heartbeat_attributes(self):
112+
def get_heartbeat_attributes(self) -> dict[str, str]:
109113
return {
110114
"partial.event": "heartbeat",
111115
"partial.frequency": str(self.heartbeat_interval_millis) + "ms",
112116
}
113117

118+
def get_log_data(self, span: Span, attributes: dict[str, str]) -> LogData:
119+
span_context = Span.get_span_context(span)
120+
121+
enc_spans = encode_spans([span]).resource_spans
122+
traces_data = trace_pb2.TracesData()
123+
traces_data.resource_spans.extend(enc_spans)
124+
serialized_traces_data = traces_data.SerializeToString()
125+
126+
log_record = LogRecord(
127+
timestamp=time.time_ns(),
128+
observed_timestamp=time.time_ns(),
129+
trace_id=span_context.trace_id,
130+
span_id=span_context.span_id,
131+
trace_flags=TraceFlags().get_default(),
132+
severity_text="INFO",
133+
severity_number=SeverityNumber.INFO,
134+
body=base64.b64encode(serialized_traces_data).decode("utf-8"),
135+
resource=self.resource,
136+
attributes=attributes,
137+
)
138+
return LogData(
139+
log_record=log_record, instrumentation_scope=span.instrumentation_scope,
140+
)
114141

115-
def get_stop_attributes():
142+
143+
def get_stop_attributes() -> dict[str, str]:
116144
return {
117145
"partial.event": "stop",
118146
}
119-
120-
121-
def get_log_data(span, attributes):
122-
span_context = Span.get_span_context(span)
123-
124-
enc_spans = encode_spans([span]).resource_spans
125-
traces_data = trace_pb2.TracesData()
126-
traces_data.resource_spans.extend(enc_spans)
127-
serialized_traces_data = traces_data.SerializeToString()
128-
129-
log_record = LogRecord(
130-
timestamp=time.time_ns(),
131-
observed_timestamp=time.time_ns(),
132-
trace_id=span_context.trace_id,
133-
span_id=span_context.span_id,
134-
trace_flags=TraceFlags().get_default(),
135-
severity_text="INFO",
136-
severity_number=SeverityNumber.INFO,
137-
body=base64.b64encode(serialized_traces_data).decode('utf-8'),
138-
attributes=attributes,
139-
)
140-
log_data = LogData(
141-
log_record=log_record, instrumentation_scope=span.instrumentation_scope
142-
)
143-
return log_data

tests/partial_span_processor/in_memory_log_exporter.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import threading
1618
import typing
1719

18-
from opentelemetry.sdk._logs import LogData
1920
from opentelemetry.sdk._logs.export import LogExporter, LogExportResult
2021

22+
if typing.TYPE_CHECKING:
23+
from opentelemetry.sdk._logs import LogData
24+
2125

2226
class InMemoryLogExporter(LogExporter):
2327
"""Implementation of :class:`.LogExporter` that stores logs in memory.
@@ -27,7 +31,7 @@ class InMemoryLogExporter(LogExporter):
2731
:func:`.get_finished_logs` method.
2832
"""
2933

30-
def __init__(self):
34+
def __init__(self) -> None:
3135
self._logs = []
3236
self._lock = threading.Lock()
3337
self._stopped = False
@@ -36,7 +40,7 @@ def clear(self) -> None:
3640
with self._lock:
3741
self._logs.clear()
3842

39-
def get_finished_logs(self) -> typing.Tuple[LogData, ...]:
43+
def get_finished_logs(self) -> tuple[LogData, ...]:
4044
with self._lock:
4145
return tuple(self._logs)
4246

tests/partial_span_processor/test_partial_span_processor.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,27 @@
1717

1818
from opentelemetry.sdk.resources import Resource
1919
from opentelemetry.sdk.trace import TracerProvider
20-
from opentelemetry.trace import SpanContext, TraceFlags
20+
from opentelemetry.trace import Span, SpanContext, TraceFlags
2121

2222
from src.partial_span_processor import PartialSpanProcessor
23-
from tests.partial_span_processor.in_memory_log_exporter import \
24-
InMemoryLogExporter
23+
from tests.partial_span_processor.in_memory_log_exporter import InMemoryLogExporter
2524

2625

2726
class TestPartialSpanProcessor(unittest.TestCase):
28-
def setUp(self):
27+
def setUp(self) -> None:
2928
# Set up an in-memory log exporter and processor
3029
self.log_exporter = InMemoryLogExporter()
3130
self.processor = PartialSpanProcessor(
3231
log_exporter=self.log_exporter,
3332
heartbeat_interval_millis=1000, # 1 second
33+
resource=Resource(attributes={"service.name": "test"}),
3434
)
3535

36-
def tearDown(self):
36+
def tearDown(self) -> None:
3737
# Shut down the processor
3838
self.processor.shutdown()
3939

40-
def create_mock_span(self, trace_id=1, span_id=1):
40+
def create_mock_span(self, trace_id: int = 1, span_id: int = 1) -> Span:
4141
# Create a mock tracer
4242
tracer_provider = TracerProvider(resource=Resource.create({}))
4343
tracer = tracer_provider.get_tracer("test_tracer")
@@ -54,36 +54,37 @@ def create_mock_span(self, trace_id=1, span_id=1):
5454
span._context = span_context # Modify the span's context for testing
5555
return span
5656

57-
def test_on_start(self):
57+
def test_on_start(self) -> None:
5858
# Test the on_start method
5959
span = self.create_mock_span()
6060
self.processor.on_start(span)
6161

6262
# Verify the span is added to active_spans
6363
span_key = (span.context.trace_id, span.context.span_id)
64-
self.assertIn(span_key, self.processor.active_spans)
64+
assert span_key in self.processor.active_spans
6565

6666
# Verify a log is emitted
6767
logs = self.log_exporter.get_finished_logs()
68-
self.assertEqual(len(logs), 1)
69-
self.assertEqual(logs[0].log_record.attributes["partial.event"],
70-
"heartbeat")
68+
assert len(logs) == 1
69+
assert logs[0].log_record.attributes["partial.event"] == "heartbeat"
70+
assert logs[0].log_record.resource.attributes["service.name"] == "test"
7171

72-
def test_on_end(self):
72+
def test_on_end(self) -> None:
7373
# Test the on_end method
7474
span = self.create_mock_span()
7575
self.processor.on_start(span)
7676
self.processor.on_end(span)
7777

7878
# Verify the span is added to ended_spans
79-
self.assertFalse(self.processor.ended_spans.empty())
79+
assert not self.processor.ended_spans.empty()
8080

8181
# Verify a log is emitted
8282
logs = self.log_exporter.get_finished_logs()
83-
self.assertEqual(len(logs), 2)
84-
self.assertEqual(logs[1].log_record.attributes["partial.event"], "stop")
83+
assert len(logs) == 2
84+
assert logs[1].log_record.attributes["partial.event"] == "stop"
85+
assert logs[0].log_record.resource.attributes["service.name"] == "test"
8586

86-
def test_heartbeat(self):
87+
def test_heartbeat(self) -> None:
8788
# Test the heartbeat method
8889
span = self.create_mock_span()
8990
self.processor.on_start(span)
@@ -93,18 +94,18 @@ def test_heartbeat(self):
9394
logs = self.log_exporter.get_finished_logs()
9495

9596
# Verify heartbeat logs are emitted
96-
self.assertGreaterEqual(len(logs), 2)
97-
self.assertEqual(logs[1].log_record.attributes["partial.event"],
98-
"heartbeat")
97+
assert len(logs) >= 2
98+
assert logs[1].log_record.attributes["partial.event"] == "heartbeat"
99+
assert logs[0].log_record.resource.attributes["service.name"] == "test"
99100

100-
def test_shutdown(self):
101+
def test_shutdown(self) -> None:
101102
# Test the shutdown method
102103
self.processor.shutdown()
103104

104105
# Verify the worker thread is stopped
105-
self.assertTrue(self.processor.done)
106+
assert self.processor.done
106107

107-
def test_worker_thread(self):
108+
def test_worker_thread(self) -> None:
108109
# Test the worker thread processes ended spans
109110
span = self.create_mock_span()
110111
self.processor.on_start(span)
@@ -115,7 +116,7 @@ def test_worker_thread(self):
115116

116117
# Verify the span is removed from active_spans
117118
span_key = (span.context.trace_id, span.context.span_id)
118-
self.assertNotIn(span_key, self.processor.active_spans)
119+
assert span_key not in self.processor.active_spans
119120

120121

121122
if __name__ == "__main__":

0 commit comments

Comments
 (0)