Skip to content

Commit b310ec1

Browse files
authored
Added exporter request methods (#212)
1 parent 3eb27ca commit b310ec1

File tree

5 files changed

+127
-30
lines changed

5 files changed

+127
-30
lines changed

exporter/opentelemetry-exporter-prometheus-remote-write/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@
77
((#206)[https://github.com/open-telemetry/opentelemetry-python-contrib/pull/206])
88
- Add conversion to TimeSeries methods
99
((#207)[https://github.com/open-telemetry/opentelemetry-python-contrib/pull/207])
10+
- Add request methods
11+
((#212)[https://github.com/open-telemetry/opentelemetry-python-contrib/pull/212])

exporter/opentelemetry-exporter-prometheus-remote-write/setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ package_dir=
3939
=src
4040
packages=find_namespace:
4141
install_requires =
42+
protobuf >= 3.13.0
43+
requests == 2.25.0
4244
opentelemetry-api == 0.17.dev0
4345
opentelemetry-sdk == 0.17.dev0
4446

exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/__init__.py

+78-18
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import re
1717
from typing import Dict, Sequence
1818

19+
import requests
20+
21+
import snappy
1922
from opentelemetry.exporter.prometheus_remote_write.gen.remote_pb2 import (
2023
WriteRequest,
2124
)
@@ -48,7 +51,7 @@ class PrometheusRemoteWriteMetricsExporter(MetricsExporter):
4851
endpoint: url where data will be sent (Required)
4952
basic_auth: username and password for authentication (Optional)
5053
headers: additional headers for remote write request (Optional)
51-
timeout: timeout for requests to the remote write endpoint in seconds (Optional)
54+
timeout: timeout for remote write requests in seconds, defaults to 30 (Optional)
5255
proxies: dict mapping request proxy protocols to proxy urls (Optional)
5356
tls_config: configuration for remote write TLS settings (Optional)
5457
"""
@@ -96,15 +99,15 @@ def basic_auth(self, basic_auth: Dict):
9699
if basic_auth:
97100
if "username" not in basic_auth:
98101
raise ValueError("username required in basic_auth")
99-
if (
100-
"password" not in basic_auth
101-
and "password_file" not in basic_auth
102-
):
102+
if "password_file" in basic_auth:
103+
if "password" in basic_auth:
104+
raise ValueError(
105+
"basic_auth cannot contain password and password_file"
106+
)
107+
with open(basic_auth["password_file"]) as file:
108+
basic_auth["password"] = file.readline().strip()
109+
elif "password" not in basic_auth:
103110
raise ValueError("password required in basic_auth")
104-
if "password" in basic_auth and "password_file" in basic_auth:
105-
raise ValueError(
106-
"basic_auth cannot contain password and password_file"
107-
)
108111
self._basic_auth = basic_auth
109112

110113
@property
@@ -159,10 +162,20 @@ def headers(self, headers: Dict):
159162
def export(
160163
self, export_records: Sequence[ExportRecord]
161164
) -> MetricsExportResult:
162-
raise NotImplementedError()
165+
if not export_records:
166+
return MetricsExportResult.SUCCESS
167+
timeseries = self._convert_to_timeseries(export_records)
168+
if not timeseries:
169+
logger.error(
170+
"All records contain unsupported aggregators, export aborted"
171+
)
172+
return MetricsExportResult.FAILURE
173+
message = self._build_message(timeseries)
174+
headers = self._build_headers()
175+
return self._send_message(message, headers)
163176

164177
def shutdown(self) -> None:
165-
raise NotImplementedError()
178+
pass
166179

167180
def _convert_to_timeseries(
168181
self, export_records: Sequence[ExportRecord]
@@ -304,13 +317,60 @@ def add_label(label_name: str, label_value: str):
304317
timeseries.samples.append(sample)
305318
return timeseries
306319

307-
def build_message(self, timeseries: Sequence[TimeSeries]) -> bytes:
308-
raise NotImplementedError()
309-
310-
def get_headers(self) -> Dict:
311-
raise NotImplementedError()
320+
def _build_message(self, timeseries: Sequence[TimeSeries]) -> bytes:
321+
write_request = WriteRequest()
322+
write_request.timeseries.extend(timeseries)
323+
serialized_message = write_request.SerializeToString()
324+
return snappy.compress(serialized_message)
325+
326+
def _build_headers(self) -> Dict:
327+
headers = {
328+
"Content-Encoding": "snappy",
329+
"Content-Type": "application/x-protobuf",
330+
"X-Prometheus-Remote-Write-Version": "0.1.0",
331+
}
332+
if self.headers:
333+
for header_name, header_value in self.headers.items():
334+
headers[header_name] = header_value
335+
return headers
312336

313-
def send_message(
337+
def _send_message(
314338
self, message: bytes, headers: Dict
315339
) -> MetricsExportResult:
316-
raise NotImplementedError()
340+
auth = None
341+
if self.basic_auth:
342+
auth = (self.basic_auth["username"], self.basic_auth["password"])
343+
344+
cert = None
345+
verify = True
346+
if self.tls_config:
347+
if "ca_file" in self.tls_config:
348+
verify = self.tls_config["ca_file"]
349+
elif "insecure_skip_verify" in self.tls_config:
350+
verify = self.tls_config["insecure_skip_verify"]
351+
352+
if (
353+
"cert_file" in self.tls_config
354+
and "key_file" in self.tls_config
355+
):
356+
cert = (
357+
self.tls_config["cert_file"],
358+
self.tls_config["key_file"],
359+
)
360+
try:
361+
response = requests.post(
362+
self.endpoint,
363+
data=message,
364+
headers=headers,
365+
auth=auth,
366+
timeout=self.timeout,
367+
proxies=self.proxies,
368+
cert=cert,
369+
verify=verify,
370+
)
371+
if not response.ok:
372+
response.raise_for_status()
373+
except requests.exceptions.RequestException as e:
374+
logger.error("Export POST request failed with reason: %s", e)
375+
return MetricsExportResult.FAILURE
376+
return MetricsExportResult.SUCCESS

exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
__version__ = "0.16.dev0"
15+
__version__ = "0.17.dev0"

exporter/opentelemetry-exporter-prometheus-remote-write/tests/test_prometheus_remote_write_exporter.py

+44-11
Original file line numberDiff line numberDiff line change
@@ -360,22 +360,55 @@ def create_label(name, value):
360360
class TestExport(unittest.TestCase):
361361
# Initializes test data that is reused across tests
362362
def setUp(self):
363-
pass
363+
self.exporter = PrometheusRemoteWriteMetricsExporter(
364+
endpoint="/prom/test_endpoint"
365+
)
364366

365367
# Ensures export is successful with valid export_records and config
366-
def test_export(self):
367-
pass
368-
369-
def test_valid_send_message(self):
370-
pass
368+
@patch("requests.post")
369+
def test_valid_export(self, mock_post):
370+
mock_post.return_value.configure_mock(**{"status_code": 200})
371+
test_metric = Counter("testname", "testdesc", "testunit", int, None)
372+
labels = get_dict_as_key({"environment": "testing"})
373+
record = ExportRecord(
374+
test_metric, labels, SumAggregator(), Resource({})
375+
)
376+
result = self.exporter.export([record])
377+
self.assertIs(result, MetricsExportResult.SUCCESS)
378+
self.assertEqual(mock_post.call_count, 1)
379+
380+
result = self.exporter.export([])
381+
self.assertIs(result, MetricsExportResult.SUCCESS)
382+
383+
def test_invalid_export(self):
384+
record = ExportRecord(None, None, None, None)
385+
result = self.exporter.export([record])
386+
self.assertIs(result, MetricsExportResult.FAILURE)
387+
388+
@patch("requests.post")
389+
def test_valid_send_message(self, mock_post):
390+
mock_post.return_value.configure_mock(**{"ok": True})
391+
result = self.exporter._send_message(bytes(), {})
392+
self.assertEqual(mock_post.call_count, 1)
393+
self.assertEqual(result, MetricsExportResult.SUCCESS)
371394

372395
def test_invalid_send_message(self):
373-
pass
396+
result = self.exporter._send_message(bytes(), {})
397+
self.assertEqual(result, MetricsExportResult.FAILURE)
374398

375399
# Verifies that build_message calls snappy.compress and returns SerializedString
376-
def test_build_message(self):
377-
pass
400+
@patch("snappy.compress", return_value=bytes())
401+
def test_build_message(self, mock_compress):
402+
message = self.exporter._build_message([TimeSeries()])
403+
self.assertEqual(mock_compress.call_count, 1)
404+
self.assertIsInstance(message, bytes)
378405

379406
# Ensure correct headers are added when valid config is provided
380-
def test_get_headers(self):
381-
pass
407+
def test_build_headers(self):
408+
self.exporter.headers = {"Custom Header": "test_header"}
409+
410+
headers = self.exporter._build_headers()
411+
self.assertEqual(headers["Content-Encoding"], "snappy")
412+
self.assertEqual(headers["Content-Type"], "application/x-protobuf")
413+
self.assertEqual(headers["X-Prometheus-Remote-Write-Version"], "0.1.0")
414+
self.assertEqual(headers["Custom Header"], "test_header")

0 commit comments

Comments
 (0)