Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The following notices are required by licensors of software used in the Snowflak
--------------------------------------------------------------------------------

This library includes software which is copied from or derived from the OpenTelemetry Python API and SDK.
OpenTelemetry Python v1.26.0
OpenTelemetry Python v1.35.0
https://github.com/open-telemetry/opentelemetry-python

Apache License
Expand Down
4 changes: 2 additions & 2 deletions anaconda/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ requirements:
- setuptools >=40.0.0
run:
- python
- opentelemetry-api ==1.26.0
- opentelemetry-sdk ==1.26.0
- opentelemetry-api ==1.35.0
- opentelemetry-sdk ==1.35.0

about:
home: https://www.snowflake.com/
Expand Down
2 changes: 1 addition & 1 deletion scripts/vendor_otlp_proto_common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# fixes needed in the OTLP exporter.

# Pinned commit/branch/tag for the current version used in opentelemetry-proto python package.
REPO_BRANCH_OR_COMMIT="v1.26.0"
REPO_BRANCH_OR_COMMIT="v1.35.0"

set -e

Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
long_description=LONG_DESCRIPTION,
python_requires=REQUIRED_PYTHON_VERSION,
install_requires=[
"opentelemetry-api == 1.26.0",
"opentelemetry-sdk == 1.26.0",
"opentelemetry-api == 1.35.0",
"opentelemetry-sdk == 1.35.0",
],
packages=find_namespace_packages(
where='src'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@

import abc
import logging
import logging.config
import threading
import typing
import opentelemetry.sdk.util.instrumentation as otel_instrumentation
import opentelemetry.sdk._logs._internal as _logs_internal

from snowflake.telemetry._internal.opentelemetry.exporter.otlp.proto.common._log_encoder import (
encode_logs,
Expand Down Expand Up @@ -84,29 +80,40 @@ class SnowflakeLoggingHandler(_logs.LoggingHandler):
discarded by the original implementation.
"""

LOGGER_NAME_TEMP_ATTRIBUTE = "__snow.logging.temp.logger_name"
CODE_FILEPATH: typing.Final = "code.filepath"
CODE_FILE_PATH: typing.Final = "code.file.path"
CODE_FUNCTION: typing.Final = "code.function"
CODE_FUNCTION_NAME: typing.Final = "code.function.name"
CODE_LINENO: typing.Final = "code.lineno"
CODE_LINE_NUMBER: typing.Final = "code.line.number"

def __init__(
self,
log_writer: LogWriter,
):
exporter = _ProtoLogExporter(log_writer)
provider = _SnowflakeTelemetryLoggerProvider()
provider.add_log_record_processor(
export.SimpleLogRecordProcessor(exporter)
processor = export.SimpleLogRecordProcessor(exporter)
provider = _logs.LoggerProvider(
resource=Resource.get_empty(),
multi_log_record_processor=processor
)
super().__init__(logger_provider=provider)

@staticmethod
def _get_attributes(record: logging.LogRecord) -> types.Attributes:
attributes = _logs.LoggingHandler._get_attributes(record) # pylint: disable=protected-access

# Temporarily storing logger's name in record's attributes.
# This attribute will be removed by the logger.
#
# TODO (SNOW-1235374): opentelemetry-python issue #2485: Record logger
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This hack can be removed after v1.28.0: open-telemetry/opentelemetry-python#4208

# name as the instrumentation scope name
attributes[SnowflakeLoggingHandler.LOGGER_NAME_TEMP_ATTRIBUTE] = record.name
# Preserving old naming conventions for code attributes that were changed as part of
# https://github.com/open-telemetry/opentelemetry-python/commit/1b1e8d80c764ad3aa76abfb56a7002ddea11fdb5 in
# order to avoid a behavior change for Snowflake customers.
if SnowflakeLoggingHandler.CODE_FILE_PATH in attributes:
attributes[SnowflakeLoggingHandler.CODE_FILEPATH] = attributes.pop(SnowflakeLoggingHandler.CODE_FILE_PATH)
if SnowflakeLoggingHandler.CODE_FUNCTION_NAME in attributes:
attributes[SnowflakeLoggingHandler.CODE_FUNCTION] = attributes.pop(
SnowflakeLoggingHandler.CODE_FUNCTION_NAME)
if SnowflakeLoggingHandler.CODE_LINE_NUMBER in attributes:
attributes[SnowflakeLoggingHandler.CODE_LINENO] = attributes.pop(SnowflakeLoggingHandler.CODE_LINE_NUMBER)

return attributes

def _translate(self, record: logging.LogRecord) -> _logs.LogRecord:
Expand All @@ -115,75 +122,6 @@ def _translate(self, record: logging.LogRecord) -> _logs.LogRecord:
return otel_record


class _SnowflakeTelemetryLogger(_logs.Logger):
"""
An Open Telemetry Logger which creates an InstrumentationScope for each
logger name it encounters.
"""

def __init__(
self,
resource: Resource,
multi_log_record_processor: typing.Union[
_logs_internal.SynchronousMultiLogRecordProcessor,
_logs_internal.ConcurrentMultiLogRecordProcessor,
],
instrumentation_scope: otel_instrumentation.InstrumentationScope,
):
super().__init__(resource, multi_log_record_processor, instrumentation_scope)
self._lock = threading.Lock()
self.cached_scopes = {}

def emit(self, record: _logs.LogRecord):
if SnowflakeLoggingHandler.LOGGER_NAME_TEMP_ATTRIBUTE not in record.attributes:
# The record doesn't contain our custom attribute with a logger name,
# so we can call the superclass's `emit` method. It will emit a log
# record with the default instrumentation scope.
super().emit(record)
return

# Creating an InstrumentationScope for each logger name,
# and caching those scopes.
logger_name = record.attributes[SnowflakeLoggingHandler.LOGGER_NAME_TEMP_ATTRIBUTE]
del record.attributes[SnowflakeLoggingHandler.LOGGER_NAME_TEMP_ATTRIBUTE]
with self._lock:
if logger_name in self.cached_scopes:
current_scope = self.cached_scopes[logger_name]
else:
current_scope = otel_instrumentation.InstrumentationScope(logger_name)
self.cached_scopes[logger_name] = current_scope

# Emitting a record with a scope that corresponds to the logger
# that logged it. NOT calling the superclass here for two reasons:
# 1. Logger.emit takes a LogRecord, not LogData.
# 2. It would emit a log record with the default instrumentation scope,
# not with the scope we want.
log_data = _logs.LogData(record, current_scope)
self._multi_log_record_processor.emit(log_data)


class _SnowflakeTelemetryLoggerProvider(_logs.LoggerProvider):
"""
A LoggerProvider that creates SnowflakeTelemetryLoggers
"""

def get_logger(
self, name: str,
version: types.Optional[str] = None,
schema_url: types.Optional[str] = None,
attributes: types.Optional[types.Attributes] = None,
) -> _logs.Logger:
return _SnowflakeTelemetryLogger(
Resource.get_empty(),
self._multi_log_record_processor,
otel_instrumentation.InstrumentationScope(
name,
version,
schema_url,
),
)


__all__ = [
"LogWriter",
"SnowflakeLoggingHandler",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#
# This file has been modified from the original source code at
#
# https://github.com/open-telemetry/opentelemetry-python/tree/v1.26.0
# https://github.com/open-telemetry/opentelemetry-python/tree/v1.35.0
#
# by Snowflake Inc.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,42 @@
#
# This file has been modified from the original source code at
#
# https://github.com/open-telemetry/opentelemetry-python/tree/v1.26.0
# https://github.com/open-telemetry/opentelemetry-python/tree/v1.35.0
#
# by Snowflake Inc.


from __future__ import annotations

import logging
from collections.abc import Sequence
from itertools import count
from typing import (
Any,
Callable,
Dict,
List,
Mapping,
Optional,
List,
Callable,
TypeVar,
Dict,
Iterator,
)

from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from snowflake.telemetry._internal.opentelemetry.proto.common.v1.common_marshaler import AnyValue as PB2AnyValue
from snowflake.telemetry._internal.opentelemetry.proto.common.v1.common_marshaler import (
InstrumentationScope as PB2InstrumentationScope,
ArrayValue as PB2ArrayValue,
)
from snowflake.telemetry._internal.opentelemetry.proto.resource.v1.resource_marshaler import (
Resource as PB2Resource,
from snowflake.telemetry._internal.opentelemetry.proto.common.v1.common_marshaler import (
InstrumentationScope as PB2InstrumentationScope,
)
from snowflake.telemetry._internal.opentelemetry.proto.common.v1.common_marshaler import AnyValue as PB2AnyValue
from snowflake.telemetry._internal.opentelemetry.proto.common.v1.common_marshaler import KeyValue as PB2KeyValue
from snowflake.telemetry._internal.opentelemetry.proto.common.v1.common_marshaler import (
KeyValueList as PB2KeyValueList,
)
from snowflake.telemetry._internal.opentelemetry.proto.common.v1.common_marshaler import (
ArrayValue as PB2ArrayValue,
from snowflake.telemetry._internal.opentelemetry.proto.resource.v1.resource_marshaler import (
Resource as PB2Resource,
)
from opentelemetry.sdk.trace import Resource
from opentelemetry.util.types import Attributes
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from opentelemetry.util.types import _ExtendedAttributes

_logger = logging.getLogger(__name__)

Expand All @@ -65,14 +65,19 @@ def _encode_instrumentation_scope(
return PB2InstrumentationScope(
name=instrumentation_scope.name,
version=instrumentation_scope.version,
attributes=_encode_attributes(instrumentation_scope.attributes),
)


def _encode_resource(resource: Resource) -> PB2Resource:
return PB2Resource(attributes=_encode_attributes(resource.attributes))


def _encode_value(value: Any) -> PB2AnyValue:
def _encode_value(
value: Any, allow_null: bool = False
) -> Optional[PB2AnyValue]:
if allow_null is True and value is None:
return None
if isinstance(value, bool):
return PB2AnyValue(bool_value=value)
if isinstance(value, str):
Expand All @@ -81,21 +86,49 @@ def _encode_value(value: Any) -> PB2AnyValue:
return PB2AnyValue(int_value=value)
if isinstance(value, float):
return PB2AnyValue(double_value=value)
if isinstance(value, bytes):
return PB2AnyValue(bytes_value=value)
if isinstance(value, Sequence):
return PB2AnyValue(
array_value=PB2ArrayValue(values=[_encode_value(v) for v in value])
array_value=PB2ArrayValue(
values=_encode_array(value, allow_null=allow_null)
)
)
elif isinstance(value, Mapping):
return PB2AnyValue(
kvlist_value=PB2KeyValueList(
values=[_encode_key_value(str(k), v) for k, v in value.items()]
values=[
_encode_key_value(str(k), v, allow_null=allow_null)
for k, v in value.items()
]
)
)
raise Exception(f"Invalid type {type(value)} of value {value}")


def _encode_key_value(key: str, value: Any) -> PB2KeyValue:
return PB2KeyValue(key=key, value=_encode_value(value))
def _encode_key_value(
key: str, value: Any, allow_null: bool = False
) -> PB2KeyValue:
return PB2KeyValue(
key=key, value=_encode_value(value, allow_null=allow_null)
)


def _encode_array(
array: Sequence[Any], allow_null: bool = False
) -> Sequence[PB2AnyValue]:
if not allow_null:
# Let the exception get raised by _encode_value()
return [_encode_value(v, allow_null=allow_null) for v in array]

return [
_encode_value(v, allow_null=allow_null)
if v is not None
# Use an empty AnyValue to represent None in an array. Behavior may change pending
# https://github.com/open-telemetry/opentelemetry-specification/issues/4392
else PB2AnyValue()
for v in array
]


def _encode_span_id(span_id: int) -> bytes:
Expand All @@ -107,14 +140,17 @@ def _encode_trace_id(trace_id: int) -> bytes:


def _encode_attributes(
attributes: Attributes,
attributes: _ExtendedAttributes,
allow_null: bool = False,
) -> Optional[List[PB2KeyValue]]:
if attributes:
pb2_attributes = []
for key, value in attributes.items():
# pylint: disable=broad-exception-caught
try:
pb2_attributes.append(_encode_key_value(key, value))
pb2_attributes.append(
_encode_key_value(key, value, allow_null=allow_null)
)
except Exception as error:
_logger.exception("Failed to encode key %s: %s", key, error)
else:
Expand Down Expand Up @@ -145,38 +181,3 @@ def _get_resource_data(
)
)
return resource_data


def _create_exp_backoff_generator(max_value: int = 0) -> Iterator[int]:
"""
Generates an infinite sequence of exponential backoff values. The sequence starts
from 1 (2^0) and doubles each time (2^1, 2^2, 2^3, ...). If a max_value is specified
and non-zero, the generated values will not exceed this maximum, capping at max_value
instead of growing indefinitely.

Parameters:
- max_value (int, optional): The maximum value to yield. If 0 or not provided, the
sequence grows without bound.

Returns:
Iterator[int]: An iterator that yields the exponential backoff values, either uncapped or
capped at max_value.

Example:
```
gen = _create_exp_backoff_generator(max_value=10)
for _ in range(5):
print(next(gen))
```
This will print:
1
2
4
8
10

Note: this functionality used to be handled by the 'backoff' package.
"""
for i in count(0):
out = 2**i
yield min(out, max_value) if max_value else out
Loading