telemetry-dev 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/PKG-INFO +10 -1
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/README.md +9 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/pyproject.toml +1 -1
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/pyproject.toml.orig +1 -1
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_client.py +7 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_context.py +77 -2
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_spans.py +48 -21
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/__init__.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_capture.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_config.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_logs.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_metrics.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_observe.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_processor.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_semconv.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/_serialize.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/otel.py +0 -0
- {telemetry_dev-0.2.1 → telemetry_dev-0.2.2}/src/telemetry_dev/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telemetry-dev
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: telemetry.dev SDK for Python — OpenTelemetry-native GenAI tracing, logs, and metrics
|
|
5
5
|
Keywords: telemetry,opentelemetry,llm,genai,tracing,observability
|
|
6
6
|
Author: telemetry.dev
|
|
@@ -80,6 +80,8 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
|
|
|
80
80
|
| `TELEMETRY_DEV_BASE_URL` | `https://ingest.telemetry.dev` | Ingest base URL (trailing slashes stripped). |
|
|
81
81
|
| `TELEMETRY_DEV_ENVIRONMENT` | `production` | Deployment environment label. |
|
|
82
82
|
| `OTEL_SERVICE_NAME` | `unknown_service` | Service name on every trace. |
|
|
83
|
+
| `OTEL_TRACES_SAMPLER` | `parentbased_always_on` | OTel trace sampling policy, which includes session roots. |
|
|
84
|
+
| `OTEL_TRACES_SAMPLER_ARG` | `1.0` | Probability for `traceidratio` / `parentbased_traceidratio`. |
|
|
83
85
|
|
|
84
86
|
Explicit `init()` arguments take precedence over environment variables.
|
|
85
87
|
|
|
@@ -137,11 +139,18 @@ Explicit `init()` arguments take precedence over environment variables.
|
|
|
137
139
|
| `mask` | `None` | `Callable[[Any, MaskContext], Any]` redaction hook, runs before JSON serialization on input/output/log messages (`MaskContext.key` is the attribute being written). Not applied to correlation identifiers. |
|
|
138
140
|
| `max_attribute_length` | `65536` | Per-content-attribute cap; truncated values get an ASCII `...[truncated]` marker appended. |
|
|
139
141
|
| `span_filter` | `None` | Export predicate `Callable[[ReadableSpan], bool]`. |
|
|
142
|
+
| `sampler` | OTel environment configuration | OTel `Sampler` that overrides environment settings. Session roots use its root policy. Real parents keep their sampling decisions. |
|
|
140
143
|
| `on_error` | `None` | Receives every internal SDK error; the SDK never raises. |
|
|
141
144
|
| `disable_atexit` | `False` | Skip the automatic atexit shutdown. |
|
|
142
145
|
| `timeout` | `10.0` | OTLP HTTP timeout in seconds. |
|
|
143
146
|
| `span_exporter`, `log_exporter`, `metric_reader` | `None` | Test seams / offline mode; any of them enables the client without an API key. |
|
|
144
147
|
|
|
148
|
+
Session roots share trace IDs from the API key and session ID. The hash matches the TypeScript SDK.
|
|
149
|
+
The SDK uses the configured OTel sampling policy, not the synthetic parent's flags.
|
|
150
|
+
For example, `init(sampler=ParentBased(TraceIdRatioBased(0.1)))` samples sessions with OTel's ratio sampler.
|
|
151
|
+
These classes come from `opentelemetry.sdk.trace.sampling`.
|
|
152
|
+
The built-in Python and TypeScript ratio samplers can select different sessions because the OTel algorithms differ.
|
|
153
|
+
|
|
145
154
|
## Auto-metrics
|
|
146
155
|
|
|
147
156
|
Ended spans automatically record two histograms (DELTA temporality, exported every 60s):
|
|
@@ -57,6 +57,8 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
|
|
|
57
57
|
| `TELEMETRY_DEV_BASE_URL` | `https://ingest.telemetry.dev` | Ingest base URL (trailing slashes stripped). |
|
|
58
58
|
| `TELEMETRY_DEV_ENVIRONMENT` | `production` | Deployment environment label. |
|
|
59
59
|
| `OTEL_SERVICE_NAME` | `unknown_service` | Service name on every trace. |
|
|
60
|
+
| `OTEL_TRACES_SAMPLER` | `parentbased_always_on` | OTel trace sampling policy, which includes session roots. |
|
|
61
|
+
| `OTEL_TRACES_SAMPLER_ARG` | `1.0` | Probability for `traceidratio` / `parentbased_traceidratio`. |
|
|
60
62
|
|
|
61
63
|
Explicit `init()` arguments take precedence over environment variables.
|
|
62
64
|
|
|
@@ -114,11 +116,18 @@ Explicit `init()` arguments take precedence over environment variables.
|
|
|
114
116
|
| `mask` | `None` | `Callable[[Any, MaskContext], Any]` redaction hook, runs before JSON serialization on input/output/log messages (`MaskContext.key` is the attribute being written). Not applied to correlation identifiers. |
|
|
115
117
|
| `max_attribute_length` | `65536` | Per-content-attribute cap; truncated values get an ASCII `...[truncated]` marker appended. |
|
|
116
118
|
| `span_filter` | `None` | Export predicate `Callable[[ReadableSpan], bool]`. |
|
|
119
|
+
| `sampler` | OTel environment configuration | OTel `Sampler` that overrides environment settings. Session roots use its root policy. Real parents keep their sampling decisions. |
|
|
117
120
|
| `on_error` | `None` | Receives every internal SDK error; the SDK never raises. |
|
|
118
121
|
| `disable_atexit` | `False` | Skip the automatic atexit shutdown. |
|
|
119
122
|
| `timeout` | `10.0` | OTLP HTTP timeout in seconds. |
|
|
120
123
|
| `span_exporter`, `log_exporter`, `metric_reader` | `None` | Test seams / offline mode; any of them enables the client without an API key. |
|
|
121
124
|
|
|
125
|
+
Session roots share trace IDs from the API key and session ID. The hash matches the TypeScript SDK.
|
|
126
|
+
The SDK uses the configured OTel sampling policy, not the synthetic parent's flags.
|
|
127
|
+
For example, `init(sampler=ParentBased(TraceIdRatioBased(0.1)))` samples sessions with OTel's ratio sampler.
|
|
128
|
+
These classes come from `opentelemetry.sdk.trace.sampling`.
|
|
129
|
+
The built-in Python and TypeScript ratio samplers can select different sessions because the OTel algorithms differ.
|
|
130
|
+
|
|
122
131
|
## Auto-metrics
|
|
123
132
|
|
|
124
133
|
Ended spans automatically record two histograms (DELTA temporality, exported every 60s):
|
|
@@ -26,6 +26,7 @@ from opentelemetry.sdk.metrics.export import (
|
|
|
26
26
|
from opentelemetry.sdk.resources import Resource
|
|
27
27
|
from opentelemetry.sdk.trace import ReadableSpan, SpanLimits, TracerProvider
|
|
28
28
|
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
29
|
+
from opentelemetry.sdk.trace.sampling import Sampler
|
|
29
30
|
from opentelemetry.trace import Tracer
|
|
30
31
|
|
|
31
32
|
from ._config import (
|
|
@@ -37,6 +38,7 @@ from ._config import (
|
|
|
37
38
|
report_error,
|
|
38
39
|
resolve_config,
|
|
39
40
|
)
|
|
41
|
+
from ._context import SessionSampler
|
|
40
42
|
from ._metrics import GuardedOTLPMetricExporter, MetricsRecorder
|
|
41
43
|
from ._processor import ExportMode, StampingSpanProcessor
|
|
42
44
|
from ._semconv import SCOPE_NAME
|
|
@@ -128,6 +130,7 @@ class Client:
|
|
|
128
130
|
mask: Mask | None = None,
|
|
129
131
|
max_attribute_length: int = 65536,
|
|
130
132
|
span_filter: Callable[[ReadableSpan], bool] | None = None,
|
|
133
|
+
sampler: Sampler | None = None,
|
|
131
134
|
on_error: Callable[[BaseException], None] | None = None,
|
|
132
135
|
disable_atexit: bool = False,
|
|
133
136
|
timeout: float = 10.0,
|
|
@@ -170,12 +173,14 @@ class Client:
|
|
|
170
173
|
# the backstop can use the same cap for raw attributes set outside the funnel.
|
|
171
174
|
self._tracer_provider = TracerProvider(
|
|
172
175
|
resource=resource,
|
|
176
|
+
sampler=sampler,
|
|
173
177
|
shutdown_on_exit=False,
|
|
174
178
|
span_limits=SpanLimits(
|
|
175
179
|
max_attribute_length=max_attribute_length,
|
|
176
180
|
max_span_attribute_length=max_attribute_length,
|
|
177
181
|
),
|
|
178
182
|
)
|
|
183
|
+
self._tracer_provider.sampler = SessionSampler(self._tracer_provider.sampler)
|
|
179
184
|
|
|
180
185
|
# Without an api key (enabled via a test seam), never construct real network
|
|
181
186
|
# exporters — they would POST to the ingest with a bogus Authorization header.
|
|
@@ -319,6 +324,7 @@ def init(
|
|
|
319
324
|
mask: Mask | None = None,
|
|
320
325
|
max_attribute_length: int = 65536,
|
|
321
326
|
span_filter: Callable[[ReadableSpan], bool] | None = None,
|
|
327
|
+
sampler: Sampler | None = None,
|
|
322
328
|
on_error: Callable[[BaseException], None] | None = None,
|
|
323
329
|
disable_atexit: bool = False,
|
|
324
330
|
timeout: float = 10.0,
|
|
@@ -352,6 +358,7 @@ def init(
|
|
|
352
358
|
mask=mask,
|
|
353
359
|
max_attribute_length=max_attribute_length,
|
|
354
360
|
span_filter=span_filter,
|
|
361
|
+
sampler=sampler,
|
|
355
362
|
on_error=on_error,
|
|
356
363
|
disable_atexit=disable_atexit,
|
|
357
364
|
timeout=timeout,
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import hashlib
|
|
4
|
+
from collections.abc import Generator, Mapping, Sequence
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
from typing import Any, cast
|
|
6
7
|
|
|
7
8
|
from opentelemetry import context as otel_context
|
|
8
9
|
from opentelemetry import trace
|
|
9
10
|
from opentelemetry.context import Context
|
|
10
|
-
from opentelemetry.trace import
|
|
11
|
+
from opentelemetry.sdk.trace.sampling import Sampler, SamplingResult
|
|
12
|
+
from opentelemetry.trace import (
|
|
13
|
+
Link,
|
|
14
|
+
NonRecordingSpan,
|
|
15
|
+
SpanContext,
|
|
16
|
+
SpanKind,
|
|
17
|
+
TraceFlags,
|
|
18
|
+
TraceState,
|
|
19
|
+
)
|
|
11
20
|
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
21
|
+
from opentelemetry.util.types import Attributes
|
|
12
22
|
|
|
13
23
|
from ._config import logger
|
|
14
24
|
from ._semconv import (
|
|
@@ -26,6 +36,41 @@ _PROPAGATOR = TraceContextTextMapPropagator()
|
|
|
26
36
|
ParentInput = str | Context | SpanContext | None
|
|
27
37
|
|
|
28
38
|
|
|
39
|
+
class _SessionParent(NonRecordingSpan):
|
|
40
|
+
def __init__(self, span_context: SpanContext, context: Context) -> None:
|
|
41
|
+
super().__init__(span_context)
|
|
42
|
+
self.parent_context = context
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SessionSampler(Sampler):
|
|
46
|
+
"""Pair with with_session_parent to sample roots using the provider's real policy."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, inner: Sampler) -> None:
|
|
49
|
+
self.inner = inner
|
|
50
|
+
|
|
51
|
+
def should_sample(
|
|
52
|
+
self,
|
|
53
|
+
parent_context: Context | None,
|
|
54
|
+
trace_id: int,
|
|
55
|
+
name: str,
|
|
56
|
+
kind: SpanKind | None = None,
|
|
57
|
+
attributes: Attributes = None,
|
|
58
|
+
links: Sequence[Link] | None = None,
|
|
59
|
+
trace_state: TraceState | None = None,
|
|
60
|
+
) -> SamplingResult:
|
|
61
|
+
parent = trace.get_current_span(parent_context)
|
|
62
|
+
if isinstance(parent, _SessionParent):
|
|
63
|
+
parent_context = trace.set_span_in_context(
|
|
64
|
+
trace.get_current_span(parent.parent_context), parent_context
|
|
65
|
+
)
|
|
66
|
+
return self.inner.should_sample(
|
|
67
|
+
parent_context, trace_id, name, kind, attributes, links, trace_state
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def get_description(self) -> str:
|
|
71
|
+
return f"SessionSampler{{{self.inner.get_description()}}}"
|
|
72
|
+
|
|
73
|
+
|
|
29
74
|
def propagated_attributes(context: Context | None = None) -> dict[str, AttributeValue]:
|
|
30
75
|
value = otel_context.get_value(_PROPAGATED_KEY, context=context)
|
|
31
76
|
if isinstance(value, Mapping):
|
|
@@ -99,3 +144,33 @@ def context_from_parent(parent: ParentInput) -> Context | None:
|
|
|
99
144
|
if ambient:
|
|
100
145
|
ctx = otel_context.set_value(_PROPAGATED_KEY, ambient, context=ctx)
|
|
101
146
|
return ctx
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def session_span_context(api_key: str | None, session_id: str) -> SpanContext:
|
|
150
|
+
"""Deterministic remote parent for a session, byte-identical to the TS `sessionSpanContext`:
|
|
151
|
+
trace id = SHA-256(api_key ‖ 0x00 ‖ session_id)[0:16], parent span id = digest[16:24].
|
|
152
|
+
The flags are not a sampling decision; pair with_session_parent with SessionSampler."""
|
|
153
|
+
text = f"{api_key or ''}\0{session_id}"
|
|
154
|
+
# Match TextEncoder: surrogate pairs combine, lone surrogates become U+FFFD.
|
|
155
|
+
text = text.encode("utf-16-le", "surrogatepass").decode("utf-16-le", "replace")
|
|
156
|
+
digest = hashlib.sha256(text.encode()).digest()
|
|
157
|
+
return SpanContext(
|
|
158
|
+
trace_id=int.from_bytes(digest[:16], "big"),
|
|
159
|
+
span_id=int.from_bytes(digest[16:24], "big"),
|
|
160
|
+
is_remote=True,
|
|
161
|
+
trace_flags=TraceFlags(TraceFlags.DEFAULT),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def with_session_parent(
|
|
166
|
+
context: Context | None, session_id: str | None, api_key: str | None
|
|
167
|
+
) -> Context | None:
|
|
168
|
+
"""Parent a root under the session; requires SessionSampler on the provider."""
|
|
169
|
+
if not api_key or not session_id:
|
|
170
|
+
return context
|
|
171
|
+
if trace.get_current_span(context).get_span_context().is_valid:
|
|
172
|
+
return context
|
|
173
|
+
base = context if context is not None else otel_context.get_current()
|
|
174
|
+
return trace.set_span_in_context(
|
|
175
|
+
_SessionParent(session_span_context(api_key, session_id), base), base
|
|
176
|
+
)
|
|
@@ -17,7 +17,12 @@ from opentelemetry.util import types as otel_types
|
|
|
17
17
|
|
|
18
18
|
from ._client import Client, get_client
|
|
19
19
|
from ._config import logger
|
|
20
|
-
from ._context import
|
|
20
|
+
from ._context import (
|
|
21
|
+
context_from_parent,
|
|
22
|
+
propagated_attributes,
|
|
23
|
+
traceparent_of,
|
|
24
|
+
with_session_parent,
|
|
25
|
+
)
|
|
21
26
|
from ._semconv import (
|
|
22
27
|
ATTR_AGENT_ID,
|
|
23
28
|
ATTR_AGENT_NAME,
|
|
@@ -30,6 +35,7 @@ from ._semconv import (
|
|
|
30
35
|
ATTR_REQUEST_MODEL,
|
|
31
36
|
ATTR_RESPONSE_ID,
|
|
32
37
|
ATTR_RESPONSE_MODEL,
|
|
38
|
+
ATTR_SESSION_ID,
|
|
33
39
|
ATTR_SYSTEM_INSTRUCTIONS,
|
|
34
40
|
ATTR_TIME_TO_FIRST_CHUNK,
|
|
35
41
|
ATTR_TOOL_CALL_ID,
|
|
@@ -145,14 +151,12 @@ def _usage_attrs(usage: Mapping[str, Any]) -> dict[str, AttributeValue]:
|
|
|
145
151
|
return attrs
|
|
146
152
|
|
|
147
153
|
|
|
148
|
-
def
|
|
154
|
+
def _fields_to_attributes(
|
|
155
|
+
client: Client, state: _SpanState, fields: dict[str, Any]
|
|
156
|
+
) -> dict[str, otel_types.AttributeValue]:
|
|
149
157
|
"""Map the shared snake_case field set onto gen_ai.* attributes; raw `attributes` merge last."""
|
|
150
158
|
attrs: dict[str, otel_types.AttributeValue] = {}
|
|
151
159
|
|
|
152
|
-
name = fields.get("name")
|
|
153
|
-
if name is not None:
|
|
154
|
-
span.update_name(name)
|
|
155
|
-
|
|
156
160
|
for field, attr in (
|
|
157
161
|
("model", ATTR_REQUEST_MODEL),
|
|
158
162
|
("provider", ATTR_PROVIDER),
|
|
@@ -222,6 +226,14 @@ def _apply_fields(client: Client, span: Span, state: _SpanState, fields: dict[st
|
|
|
222
226
|
if attr is not None:
|
|
223
227
|
attrs[key] = attr
|
|
224
228
|
|
|
229
|
+
return attrs
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _apply_fields(client: Client, span: Span, state: _SpanState, fields: dict[str, Any]) -> None:
|
|
233
|
+
name = fields.get("name")
|
|
234
|
+
if name is not None:
|
|
235
|
+
span.update_name(name)
|
|
236
|
+
attrs = _fields_to_attributes(client, state, fields)
|
|
225
237
|
if attrs:
|
|
226
238
|
span.set_attributes(attrs)
|
|
227
239
|
|
|
@@ -538,19 +550,6 @@ def start_span(
|
|
|
538
550
|
capture_input=client.capture_input if capture_input is None else capture_input,
|
|
539
551
|
capture_output=client.capture_output if capture_output is None else capture_output,
|
|
540
552
|
)
|
|
541
|
-
initial: dict[str, otel_types.AttributeValue] = {ATTR_OPERATION: operation}
|
|
542
|
-
if operation == "execute_tool":
|
|
543
|
-
initial[ATTR_TOOL_NAME] = name
|
|
544
|
-
elif operation == "invoke_agent":
|
|
545
|
-
initial[ATTR_AGENT_NAME] = name
|
|
546
|
-
span = client.tracer.start_span(
|
|
547
|
-
name,
|
|
548
|
-
context=context_from_parent(parent),
|
|
549
|
-
attributes=initial,
|
|
550
|
-
start_time=_to_ns(start_time),
|
|
551
|
-
)
|
|
552
|
-
_SPAN_STATES[span] = state
|
|
553
|
-
handle = SpanHandle(span, client, state)
|
|
554
553
|
fields = _collect_fields(
|
|
555
554
|
input=input,
|
|
556
555
|
output=output,
|
|
@@ -580,8 +579,36 @@ def start_span(
|
|
|
580
579
|
metadata=metadata,
|
|
581
580
|
attributes=attributes,
|
|
582
581
|
)
|
|
583
|
-
|
|
584
|
-
|
|
582
|
+
attrs = _fields_to_attributes(client, state, fields)
|
|
583
|
+
initial: dict[str, otel_types.AttributeValue] = {ATTR_OPERATION: operation}
|
|
584
|
+
if operation == "execute_tool":
|
|
585
|
+
initial[ATTR_TOOL_NAME] = name
|
|
586
|
+
elif operation == "invoke_agent":
|
|
587
|
+
initial[ATTR_AGENT_NAME] = name
|
|
588
|
+
ctx = context_from_parent(parent)
|
|
589
|
+
propagated = propagated_attributes(ctx)
|
|
590
|
+
explicit_session_id = (attributes or {}).get(ATTR_SESSION_ID)
|
|
591
|
+
session_id = (
|
|
592
|
+
explicit_session_id
|
|
593
|
+
if isinstance(explicit_session_id, str)
|
|
594
|
+
else propagated.get(ATTR_SESSION_ID)
|
|
595
|
+
)
|
|
596
|
+
ctx = with_session_parent(
|
|
597
|
+
ctx, session_id if isinstance(session_id, str) else None, client.config.api_key
|
|
598
|
+
)
|
|
599
|
+
initial.update(propagated)
|
|
600
|
+
initial.update(attrs)
|
|
601
|
+
span = client.tracer.start_span(
|
|
602
|
+
name,
|
|
603
|
+
context=ctx,
|
|
604
|
+
attributes=initial,
|
|
605
|
+
start_time=_to_ns(start_time),
|
|
606
|
+
)
|
|
607
|
+
_SPAN_STATES[span] = state
|
|
608
|
+
handle = SpanHandle(span, client, state)
|
|
609
|
+
# The processor stamps propagation on start; explicit fields still win.
|
|
610
|
+
if attrs:
|
|
611
|
+
span.set_attributes(attrs)
|
|
585
612
|
return handle
|
|
586
613
|
except BaseException as exc:
|
|
587
614
|
client.report("start_span failed", exc)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|