Skip to content

Commit 0dad10e

Browse files
committed
Added export request methods
1 parent f71bc2b commit 0dad10e

File tree

3 files changed

+129
-30
lines changed

3 files changed

+129
-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/src/opentelemetry/exporter/prometheus_remote_write/__init__.py

+75-17
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
)
@@ -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,16 @@ def headers(self, headers: Dict):
159162
def export(
160163
self, export_records: Sequence[ExportRecord]
161164
) -> MetricsExportResult:
162-
raise NotImplementedError()
165+
timeseries = self._convert_to_timeseries(export_records)
166+
if not timeseries:
167+
logger.warning("No valid records found, export aborted")
168+
return MetricsExportResult.FAILURE
169+
message = self._build_message(timeseries)
170+
headers = self._build_headers()
171+
return self._send_message(message, headers)
163172

164173
def shutdown(self) -> None:
165-
raise NotImplementedError()
174+
pass
166175

167176
def _convert_to_timeseries(
168177
self, export_records: Sequence[ExportRecord]
@@ -304,13 +313,62 @@ def add_label(label_name: str, label_value: str):
304313
timeseries.samples.append(sample)
305314
return timeseries
306315

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

313-
def send_message(
333+
def _send_message(
314334
self, message: bytes, headers: Dict
315335
) -> MetricsExportResult:
316-
raise NotImplementedError()
336+
auth = None
337+
if self.basic_auth:
338+
auth = (self.basic_auth["username"], self.basic_auth["password"])
339+
340+
cert = None
341+
verify = True
342+
if self.tls_config:
343+
if "ca_file" in self.tls_config:
344+
verify = self.tls_config["ca_file"]
345+
elif "insecure_skip_verify" in self.tls_config:
346+
verify = self.tls_config["insecure_skip_verify"]
347+
348+
if (
349+
"cert_file" in self.tls_config
350+
and "key_file" in self.tls_config
351+
):
352+
cert = (
353+
self.tls_config["cert_file"],
354+
self.tls_config["key_file"],
355+
)
356+
response = requests.post(
357+
self.endpoint,
358+
data=message,
359+
headers=headers,
360+
auth=auth,
361+
timeout=self.timeout,
362+
proxies=self.proxies,
363+
cert=cert,
364+
verify=verify,
365+
)
366+
if response.status_code != 200:
367+
logger.warning(
368+
"POST request failed with status %s with reason: %s and content: %s",
369+
str(response.status_code),
370+
response.reason,
371+
str(response.content),
372+
)
373+
return MetricsExportResult.FAILURE
374+
return MetricsExportResult.SUCCESS

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

+52-13
Original file line numberDiff line numberDiff line change
@@ -360,22 +360,61 @@ 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
371-
372-
def test_invalid_send_message(self):
373-
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+
def test_invalid_export(self):
381+
record = ExportRecord(None, None, None, None)
382+
result = self.exporter.export([record])
383+
self.assertIs(result, MetricsExportResult.FAILURE)
384+
385+
@patch("requests.post")
386+
def test_valid_send_message(self, mock_post):
387+
mock_post.return_value.configure_mock(**{"status_code": 200})
388+
result = self.exporter._send_message(bytes(), {})
389+
self.assertEqual(mock_post.call_count, 1)
390+
self.assertEqual(result, MetricsExportResult.SUCCESS)
391+
392+
@patch("requests.post")
393+
def test_invalid_send_message(self, mock_post):
394+
mock_post.return_value.configure_mock(
395+
**{
396+
"status_code": 404,
397+
"reason": "test_reason",
398+
"content": "test_content",
399+
}
400+
)
401+
result = self.exporter._send_message(bytes(), {})
402+
self.assertEqual(mock_post.call_count, 1)
403+
self.assertEqual(result, MetricsExportResult.FAILURE)
374404

375405
# Verifies that build_message calls snappy.compress and returns SerializedString
376-
def test_build_message(self):
377-
pass
406+
@patch("snappy.compress", return_value=bytes())
407+
def test_build_message(self, mock_compress):
408+
message = self.exporter._build_message([TimeSeries()])
409+
self.assertEqual(mock_compress.call_count, 1)
410+
self.assertIsInstance(message, bytes)
378411

379412
# Ensure correct headers are added when valid config is provided
380-
def test_get_headers(self):
381-
pass
413+
def test_build_headers(self):
414+
self.exporter.headers = {"Custom Header": "test_header"}
415+
416+
headers = self.exporter._build_headers()
417+
self.assertEqual(headers["Content-Encoding"], "snappy")
418+
self.assertEqual(headers["Content-Type"], "application/x-protobuf")
419+
self.assertEqual(headers["X-Prometheus-Remote-Write-Version"], "0.1.0")
420+
self.assertEqual(headers["Custom Header"], "test_header")

0 commit comments

Comments
 (0)