Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement Client Key and Certificate File Support for All OTLP Export…
Browse files Browse the repository at this point in the history
sandy2008 authored and hyoinandout committed Aug 29, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent c94095f commit e746026
Showing 17 changed files with 478 additions and 31 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4103](https://github.com/open-telemetry/opentelemetry-python/pull/4103))
- Update semantic conventions to version 1.27.0
([#4104](https://github.com/open-telemetry/opentelemetry-python/pull/4104))
- Implement Client Key and Certificate File Support for All OTLP Exporters
([#4116](https://github.com/open-telemetry/opentelemetry-python/pull/4116))

## Version 1.26.0/0.47b0 (2024-07-25)

Original file line number Diff line number Diff line change
@@ -34,6 +34,8 @@

from opentelemetry.sdk.environment_variables import (
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE,
OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY,
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
OTEL_EXPORTER_OTLP_LOGS_HEADERS,
@@ -71,7 +73,10 @@ def __init__(
and environ.get(OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE) is not None
):
credentials = _get_credentials(
credentials, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE
credentials,
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY,
OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE,
)

environ_timeout = environ.get(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT)
Original file line number Diff line number Diff line change
@@ -61,6 +61,8 @@
)
from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401
from opentelemetry.sdk.environment_variables import (
OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,
OTEL_EXPORTER_OTLP_CLIENT_KEY,
OTEL_EXPORTER_OTLP_CERTIFICATE,
OTEL_EXPORTER_OTLP_COMPRESSION,
OTEL_EXPORTER_OTLP_ENDPOINT,
@@ -118,22 +120,55 @@ def get_resource_data(
return _get_resource_data(sdk_resource_scope_data, resource_class, name)


def _load_credential_from_file(filepath) -> ChannelCredentials:
def _read_file(file_path: str) -> Optional[bytes]:
try:
with open(filepath, "rb") as creds_file:
credential = creds_file.read()
return ssl_channel_credentials(credential)
except FileNotFoundError:
logger.exception("Failed to read credential file")
with open(file_path, "rb") as file:
return file.read()
except FileNotFoundError as e:
logger.exception(
f"Failed to read file: {e.filename}. Please check if the file exists and is accessible."
)
return None


def _get_credentials(creds, environ_key):
def _load_credentials(
certificate_file: Optional[str],
client_key_file: Optional[str],
client_certificate_file: Optional[str],
) -> Optional[ChannelCredentials]:
root_certificates = (
_read_file(certificate_file) if certificate_file else None
)
private_key = _read_file(client_key_file) if client_key_file else None
certificate_chain = (
_read_file(client_certificate_file)
if client_certificate_file
else None
)

return ssl_channel_credentials(
root_certificates=root_certificates,
private_key=private_key,
certificate_chain=certificate_chain,
)


def _get_credentials(
creds: Optional[ChannelCredentials],
certificate_file_env_key: str,
client_key_file_env_key: str,
client_certificate_file_env_key: str,
) -> ChannelCredentials:
if creds is not None:
return creds
creds_env = environ.get(environ_key)
if creds_env:
return _load_credential_from_file(creds_env)

certificate_file = environ.get(certificate_file_env_key)
if certificate_file:
client_key_file = environ.get(client_key_file_env_key)
client_certificate_file = environ.get(client_certificate_file_env_key)
return _load_credentials(
certificate_file, client_key_file, client_certificate_file
)
return ssl_channel_credentials()


@@ -214,7 +249,10 @@ def __init__(
)
else:
credentials = _get_credentials(
credentials, OTEL_EXPORTER_OTLP_CERTIFICATE
credentials,
OTEL_EXPORTER_OTLP_CERTIFICATE,
OTEL_EXPORTER_OTLP_CLIENT_KEY,
OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,
)
self._client = self._stub(
secure_channel(
Original file line number Diff line number Diff line change
@@ -42,6 +42,8 @@
from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 # noqa: F401
from opentelemetry.sdk.environment_variables import (
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE,
OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE,
OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY,
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION,
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
OTEL_EXPORTER_OTLP_METRICS_HEADERS,
@@ -113,7 +115,10 @@ def __init__(
and environ.get(OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE) is not None
):
credentials = _get_credentials(
credentials, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE
credentials,
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE,
OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY,
OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE,
)

environ_timeout = environ.get(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT)
Original file line number Diff line number Diff line change
@@ -48,6 +48,8 @@
)
from opentelemetry.proto.trace.v1.trace_pb2 import Status # noqa: F401
from opentelemetry.sdk.environment_variables import (
OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE,
OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY,
OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE,
OTEL_EXPORTER_OTLP_TRACES_COMPRESSION,
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
@@ -105,7 +107,10 @@ def __init__(
and environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) is not None
):
credentials = _get_credentials(
credentials, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE
credentials,
OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE,
OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY,
OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE,
)

environ_timeout = environ.get(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT)
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# pylint: disable=too-many-lines

import time
from concurrent.futures import ThreadPoolExecutor
from os.path import dirname
@@ -53,6 +55,8 @@
from opentelemetry.sdk._logs.export import LogExportResult
from opentelemetry.sdk.environment_variables import (
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE,
OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY,
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
OTEL_EXPORTER_OTLP_LOGS_HEADERS,
@@ -206,12 +210,40 @@ def test_exporting(self):
# pylint: disable=protected-access
self.assertEqual(self.exporter._exporting, "logs")

@patch.dict(
"os.environ",
{
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317",
OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2",
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10",
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip",
},
)
@patch(
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
)
def test_env_variables(self, mock_exporter_mixin):
OTLPLogExporter()

self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
_, kwargs = mock_exporter_mixin.call_args_list[0]
self.assertEqual(kwargs["endpoint"], "logs:4317")
self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2")
self.assertEqual(kwargs["timeout"], 10)
self.assertEqual(kwargs["compression"], Compression.Gzip)
self.assertIsNone(kwargs["credentials"])

# Create a new test method specifically for client certificates
@patch.dict(
"os.environ",
{
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317",
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR
+ "/../fixtures/test.cert",
OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE: THIS_DIR
+ "/../fixtures/test-client-cert.pem",
OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY: THIS_DIR
+ "/../fixtures/test-client-key.pem",
OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2",
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10",
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip",
@@ -220,7 +252,7 @@ def test_exporting(self):
@patch(
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
)
def test_env_variables(self, mock_exporter_mixin):
def test_env_variables_with_client_certificates(self, mock_exporter_mixin):
OTLPLogExporter()

self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
@@ -232,6 +264,37 @@ def test_env_variables(self, mock_exporter_mixin):
self.assertIsNotNone(kwargs["credentials"])
self.assertIsInstance(kwargs["credentials"], ChannelCredentials)

@patch.dict(
"os.environ",
{
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317",
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR
+ "/../fixtures/test.cert",
OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2",
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10",
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip",
},
)
@patch(
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
)
@patch("logging.Logger.error")
def test_env_variables_with_only_certificate(
self, mock_logger_error, mock_exporter_mixin
):
OTLPLogExporter()

self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
_, kwargs = mock_exporter_mixin.call_args_list[0]
self.assertEqual(kwargs["endpoint"], "logs:4317")
self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2")
self.assertEqual(kwargs["timeout"], 10)
self.assertEqual(kwargs["compression"], Compression.Gzip)
self.assertIsNotNone(kwargs["credentials"])
self.assertIsInstance(kwargs["credentials"], ChannelCredentials)

mock_logger_error.assert_not_called()

@patch(
"opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials"
)
Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# pylint: disable=too-many-lines

import threading
from concurrent.futures import ThreadPoolExecutor

@@ -45,6 +47,8 @@
from opentelemetry.sdk.environment_variables import (
OTEL_EXPORTER_OTLP_COMPRESSION,
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE,
OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE,
OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY,
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION,
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
@@ -217,12 +221,40 @@ def test_preferred_temporality(self):
AggregationTemporality.CUMULATIVE,
)

@patch.dict(
"os.environ",
{
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317",
OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2",
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10",
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip",
},
)
@patch(
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
)
def test_env_variables(self, mock_exporter_mixin):
OTLPMetricExporter()

self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
_, kwargs = mock_exporter_mixin.call_args_list[0]

self.assertEqual(kwargs["endpoint"], "collector:4317")
self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2")
self.assertEqual(kwargs["timeout"], 10)
self.assertEqual(kwargs["compression"], Compression.Gzip)
self.assertIsNone(kwargs["credentials"])

@patch.dict(
"os.environ",
{
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317",
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: THIS_DIR
+ "/fixtures/test.cert",
OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: THIS_DIR
+ "/fixtures/test-client-cert.pem",
OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: THIS_DIR
+ "/fixtures/test-client-key.pem",
OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2",
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10",
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip",
@@ -231,7 +263,7 @@ def test_preferred_temporality(self):
@patch(
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
)
def test_env_variables(self, mock_exporter_mixin):
def test_env_variables_with_client_certificates(self, mock_exporter_mixin):
OTLPMetricExporter()

self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
@@ -244,6 +276,37 @@ def test_env_variables(self, mock_exporter_mixin):
self.assertIsNotNone(kwargs["credentials"])
self.assertIsInstance(kwargs["credentials"], ChannelCredentials)

@patch.dict(
"os.environ",
{
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317",
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: THIS_DIR
+ "/fixtures/test.cert",
OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2",
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10",
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip",
},
)
@patch(
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
)
@patch("logging.Logger.error")
def test_env_variables_with_only_certificate(
self, mock_logger_error, mock_exporter_mixin
):
OTLPMetricExporter()

self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
_, kwargs = mock_exporter_mixin.call_args_list[0]
self.assertEqual(kwargs["endpoint"], "collector:4317")
self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2")
self.assertEqual(kwargs["timeout"], 10)
self.assertEqual(kwargs["compression"], Compression.Gzip)
self.assertIsNotNone(kwargs["credentials"])
self.assertIsInstance(kwargs["credentials"], ChannelCredentials)

mock_logger_error.assert_not_called()

@patch(
"opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials"
)
Loading

0 comments on commit e746026

Please sign in to comment.