Skip to content

Commit

Permalink
Implement Client Key and Certificate File Support for All OTLP Export…
Browse files Browse the repository at this point in the history
…ers (#4116)
  • Loading branch information
sandy2008 authored Aug 20, 2024
1 parent ea4616d commit e8cf94c
Show file tree
Hide file tree
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
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()


Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -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)
Expand All @@ -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"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -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)
Expand All @@ -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"
)
Expand Down
Loading

0 comments on commit e8cf94c

Please sign in to comment.