Skip to content

Commit e8cf94c

Browse files
authored
Implement Client Key and Certificate File Support for All OTLP Exporters (#4116)
1 parent ea4616d commit e8cf94c

File tree

17 files changed

+478
-31
lines changed

17 files changed

+478
-31
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
([#4103](https://github.com/open-telemetry/opentelemetry-python/pull/4103))
2626
- Update semantic conventions to version 1.27.0
2727
([#4104](https://github.com/open-telemetry/opentelemetry-python/pull/4104))
28+
- Implement Client Key and Certificate File Support for All OTLP Exporters
29+
([#4116](https://github.com/open-telemetry/opentelemetry-python/pull/4116))
2830

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

exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434

3535
from opentelemetry.sdk.environment_variables import (
3636
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
37+
OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE,
38+
OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY,
3739
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
3840
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
3941
OTEL_EXPORTER_OTLP_LOGS_HEADERS,
@@ -71,7 +73,10 @@ def __init__(
7173
and environ.get(OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE) is not None
7274
):
7375
credentials = _get_credentials(
74-
credentials, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE
76+
credentials,
77+
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
78+
OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY,
79+
OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE,
7580
)
7681

7782
environ_timeout = environ.get(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT)

exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
)
6262
from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401
6363
from opentelemetry.sdk.environment_variables import (
64+
OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,
65+
OTEL_EXPORTER_OTLP_CLIENT_KEY,
6466
OTEL_EXPORTER_OTLP_CERTIFICATE,
6567
OTEL_EXPORTER_OTLP_COMPRESSION,
6668
OTEL_EXPORTER_OTLP_ENDPOINT,
@@ -118,22 +120,55 @@ def get_resource_data(
118120
return _get_resource_data(sdk_resource_scope_data, resource_class, name)
119121

120122

121-
def _load_credential_from_file(filepath) -> ChannelCredentials:
123+
def _read_file(file_path: str) -> Optional[bytes]:
122124
try:
123-
with open(filepath, "rb") as creds_file:
124-
credential = creds_file.read()
125-
return ssl_channel_credentials(credential)
126-
except FileNotFoundError:
127-
logger.exception("Failed to read credential file")
125+
with open(file_path, "rb") as file:
126+
return file.read()
127+
except FileNotFoundError as e:
128+
logger.exception(
129+
f"Failed to read file: {e.filename}. Please check if the file exists and is accessible."
130+
)
128131
return None
129132

130133

131-
def _get_credentials(creds, environ_key):
134+
def _load_credentials(
135+
certificate_file: Optional[str],
136+
client_key_file: Optional[str],
137+
client_certificate_file: Optional[str],
138+
) -> Optional[ChannelCredentials]:
139+
root_certificates = (
140+
_read_file(certificate_file) if certificate_file else None
141+
)
142+
private_key = _read_file(client_key_file) if client_key_file else None
143+
certificate_chain = (
144+
_read_file(client_certificate_file)
145+
if client_certificate_file
146+
else None
147+
)
148+
149+
return ssl_channel_credentials(
150+
root_certificates=root_certificates,
151+
private_key=private_key,
152+
certificate_chain=certificate_chain,
153+
)
154+
155+
156+
def _get_credentials(
157+
creds: Optional[ChannelCredentials],
158+
certificate_file_env_key: str,
159+
client_key_file_env_key: str,
160+
client_certificate_file_env_key: str,
161+
) -> ChannelCredentials:
132162
if creds is not None:
133163
return creds
134-
creds_env = environ.get(environ_key)
135-
if creds_env:
136-
return _load_credential_from_file(creds_env)
164+
165+
certificate_file = environ.get(certificate_file_env_key)
166+
if certificate_file:
167+
client_key_file = environ.get(client_key_file_env_key)
168+
client_certificate_file = environ.get(client_certificate_file_env_key)
169+
return _load_credentials(
170+
certificate_file, client_key_file, client_certificate_file
171+
)
137172
return ssl_channel_credentials()
138173

139174

@@ -214,7 +249,10 @@ def __init__(
214249
)
215250
else:
216251
credentials = _get_credentials(
217-
credentials, OTEL_EXPORTER_OTLP_CERTIFICATE
252+
credentials,
253+
OTEL_EXPORTER_OTLP_CERTIFICATE,
254+
OTEL_EXPORTER_OTLP_CLIENT_KEY,
255+
OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,
218256
)
219257
self._client = self._stub(
220258
secure_channel(

exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 # noqa: F401
4343
from opentelemetry.sdk.environment_variables import (
4444
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE,
45+
OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE,
46+
OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY,
4547
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION,
4648
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
4749
OTEL_EXPORTER_OTLP_METRICS_HEADERS,
@@ -113,7 +115,10 @@ def __init__(
113115
and environ.get(OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE) is not None
114116
):
115117
credentials = _get_credentials(
116-
credentials, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE
118+
credentials,
119+
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE,
120+
OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY,
121+
OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE,
117122
)
118123

119124
environ_timeout = environ.get(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT)

exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
)
4949
from opentelemetry.proto.trace.v1.trace_pb2 import Status # noqa: F401
5050
from opentelemetry.sdk.environment_variables import (
51+
OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE,
52+
OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY,
5153
OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE,
5254
OTEL_EXPORTER_OTLP_TRACES_COMPRESSION,
5355
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
@@ -105,7 +107,10 @@ def __init__(
105107
and environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) is not None
106108
):
107109
credentials = _get_credentials(
108-
credentials, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE
110+
credentials,
111+
OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE,
112+
OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY,
113+
OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE,
109114
)
110115

111116
environ_timeout = environ.get(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT)

exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test-client-cert.pem

Whitespace-only changes.

exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test-client-key.pem

Whitespace-only changes.

exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
# pylint: disable=too-many-lines
16+
1517
import time
1618
from concurrent.futures import ThreadPoolExecutor
1719
from os.path import dirname
@@ -53,6 +55,8 @@
5355
from opentelemetry.sdk._logs.export import LogExportResult
5456
from opentelemetry.sdk.environment_variables import (
5557
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
58+
OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE,
59+
OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY,
5660
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
5761
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
5862
OTEL_EXPORTER_OTLP_LOGS_HEADERS,
@@ -206,12 +210,40 @@ def test_exporting(self):
206210
# pylint: disable=protected-access
207211
self.assertEqual(self.exporter._exporting, "logs")
208212

213+
@patch.dict(
214+
"os.environ",
215+
{
216+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317",
217+
OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2",
218+
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10",
219+
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip",
220+
},
221+
)
222+
@patch(
223+
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
224+
)
225+
def test_env_variables(self, mock_exporter_mixin):
226+
OTLPLogExporter()
227+
228+
self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
229+
_, kwargs = mock_exporter_mixin.call_args_list[0]
230+
self.assertEqual(kwargs["endpoint"], "logs:4317")
231+
self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2")
232+
self.assertEqual(kwargs["timeout"], 10)
233+
self.assertEqual(kwargs["compression"], Compression.Gzip)
234+
self.assertIsNone(kwargs["credentials"])
235+
236+
# Create a new test method specifically for client certificates
209237
@patch.dict(
210238
"os.environ",
211239
{
212240
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317",
213241
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR
214242
+ "/../fixtures/test.cert",
243+
OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE: THIS_DIR
244+
+ "/../fixtures/test-client-cert.pem",
245+
OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY: THIS_DIR
246+
+ "/../fixtures/test-client-key.pem",
215247
OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2",
216248
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10",
217249
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip",
@@ -220,7 +252,7 @@ def test_exporting(self):
220252
@patch(
221253
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
222254
)
223-
def test_env_variables(self, mock_exporter_mixin):
255+
def test_env_variables_with_client_certificates(self, mock_exporter_mixin):
224256
OTLPLogExporter()
225257

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

267+
@patch.dict(
268+
"os.environ",
269+
{
270+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317",
271+
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR
272+
+ "/../fixtures/test.cert",
273+
OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2",
274+
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10",
275+
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip",
276+
},
277+
)
278+
@patch(
279+
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
280+
)
281+
@patch("logging.Logger.error")
282+
def test_env_variables_with_only_certificate(
283+
self, mock_logger_error, mock_exporter_mixin
284+
):
285+
OTLPLogExporter()
286+
287+
self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
288+
_, kwargs = mock_exporter_mixin.call_args_list[0]
289+
self.assertEqual(kwargs["endpoint"], "logs:4317")
290+
self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2")
291+
self.assertEqual(kwargs["timeout"], 10)
292+
self.assertEqual(kwargs["compression"], Compression.Gzip)
293+
self.assertIsNotNone(kwargs["credentials"])
294+
self.assertIsInstance(kwargs["credentials"], ChannelCredentials)
295+
296+
mock_logger_error.assert_not_called()
297+
235298
@patch(
236299
"opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials"
237300
)

exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
# pylint: disable=too-many-lines
16+
1517
import threading
1618
from concurrent.futures import ThreadPoolExecutor
1719

@@ -45,6 +47,8 @@
4547
from opentelemetry.sdk.environment_variables import (
4648
OTEL_EXPORTER_OTLP_COMPRESSION,
4749
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE,
50+
OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE,
51+
OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY,
4852
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION,
4953
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
5054
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
@@ -217,12 +221,40 @@ def test_preferred_temporality(self):
217221
AggregationTemporality.CUMULATIVE,
218222
)
219223

224+
@patch.dict(
225+
"os.environ",
226+
{
227+
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317",
228+
OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2",
229+
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10",
230+
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip",
231+
},
232+
)
233+
@patch(
234+
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
235+
)
236+
def test_env_variables(self, mock_exporter_mixin):
237+
OTLPMetricExporter()
238+
239+
self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
240+
_, kwargs = mock_exporter_mixin.call_args_list[0]
241+
242+
self.assertEqual(kwargs["endpoint"], "collector:4317")
243+
self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2")
244+
self.assertEqual(kwargs["timeout"], 10)
245+
self.assertEqual(kwargs["compression"], Compression.Gzip)
246+
self.assertIsNone(kwargs["credentials"])
247+
220248
@patch.dict(
221249
"os.environ",
222250
{
223251
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317",
224252
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: THIS_DIR
225253
+ "/fixtures/test.cert",
254+
OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: THIS_DIR
255+
+ "/fixtures/test-client-cert.pem",
256+
OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: THIS_DIR
257+
+ "/fixtures/test-client-key.pem",
226258
OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2",
227259
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10",
228260
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip",
@@ -231,7 +263,7 @@ def test_preferred_temporality(self):
231263
@patch(
232264
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
233265
)
234-
def test_env_variables(self, mock_exporter_mixin):
266+
def test_env_variables_with_client_certificates(self, mock_exporter_mixin):
235267
OTLPMetricExporter()
236268

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

279+
@patch.dict(
280+
"os.environ",
281+
{
282+
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317",
283+
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: THIS_DIR
284+
+ "/fixtures/test.cert",
285+
OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2",
286+
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10",
287+
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip",
288+
},
289+
)
290+
@patch(
291+
"opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
292+
)
293+
@patch("logging.Logger.error")
294+
def test_env_variables_with_only_certificate(
295+
self, mock_logger_error, mock_exporter_mixin
296+
):
297+
OTLPMetricExporter()
298+
299+
self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
300+
_, kwargs = mock_exporter_mixin.call_args_list[0]
301+
self.assertEqual(kwargs["endpoint"], "collector:4317")
302+
self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2")
303+
self.assertEqual(kwargs["timeout"], 10)
304+
self.assertEqual(kwargs["compression"], Compression.Gzip)
305+
self.assertIsNotNone(kwargs["credentials"])
306+
self.assertIsInstance(kwargs["credentials"], ChannelCredentials)
307+
308+
mock_logger_error.assert_not_called()
309+
247310
@patch(
248311
"opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials"
249312
)

0 commit comments

Comments
 (0)