Skip to content

Commit 4d581e9

Browse files
authored
Set gRPC user-agent when calling google APIs (#216)
1 parent b2490ed commit 4d581e9

File tree

6 files changed

+139
-35
lines changed

6 files changed

+139
-35
lines changed

opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
from typing import Dict, List, NoReturn, Optional, Set, Union
2020

2121
import google.auth
22+
import pkg_resources
2223
from google.api.distribution_pb2 import Distribution
2324
from google.api.label_pb2 import LabelDescriptor
2425
from google.api.metric_pb2 import Metric as GMetric
2526
from google.api.metric_pb2 import MetricDescriptor
26-
from google.api.monitored_resource_pb2 import MonitoredResource
2727
from google.cloud.monitoring_v3 import (
2828
CreateMetricDescriptorRequest,
2929
CreateTimeSeriesRequest,
@@ -33,12 +33,16 @@
3333
TimeSeries,
3434
TypedValue,
3535
)
36+
from google.cloud.monitoring_v3.services.metric_service.transports.grpc import (
37+
MetricServiceGrpcTransport,
38+
)
3639

3740
# pylint: disable=no-name-in-module
3841
from google.protobuf.timestamp_pb2 import Timestamp
3942
from opentelemetry.exporter.cloud_monitoring._resource import (
4043
get_monitored_resource,
4144
)
45+
from opentelemetry.exporter.cloud_monitoring.version import __version__
4246
from opentelemetry.sdk.metrics.export import (
4347
Gauge,
4448
Histogram,
@@ -50,14 +54,25 @@
5054
NumberDataPoint,
5155
Sum,
5256
)
53-
from opentelemetry.sdk.resources import Resource
5457

5558
logger = logging.getLogger(__name__)
5659
MAX_BATCH_WRITE = 200
5760
WRITE_INTERVAL = 10
5861
UNIQUE_IDENTIFIER_KEY = "opentelemetry_id"
5962
NANOS_PER_SECOND = 10**9
6063

64+
_OTEL_SDK_VERSION = pkg_resources.get_distribution("opentelemetry-sdk").version
65+
_USER_AGENT = f"opentelemetry-python {_OTEL_SDK_VERSION}; google-cloud-metric-exporter {__version__}"
66+
67+
# Set user-agent metadata, see https://github.com/grpc/grpc/issues/23644 and default options
68+
# from
69+
# https://github.com/googleapis/python-monitoring/blob/v2.11.3/google/cloud/monitoring_v3/services/metric_service/transports/grpc.py#L175-L178
70+
_OPTIONS = [
71+
("grpc.max_send_message_length", -1),
72+
("grpc.max_receive_message_length", -1),
73+
("grpc.primary_user_agent", _USER_AGENT),
74+
]
75+
6176

6277
# pylint is unable to resolve members of protobuf objects
6378
# pylint: disable=no-member
@@ -88,7 +103,13 @@ def __init__(
88103
# Default preferred_temporality is all CUMULATIVE so need to customize
89104
super().__init__()
90105

91-
self.client = client or MetricServiceClient()
106+
self.client = client or MetricServiceClient(
107+
transport=MetricServiceGrpcTransport(
108+
channel=MetricServiceGrpcTransport.create_channel(
109+
options=_OPTIONS,
110+
)
111+
)
112+
)
92113
self.project_id: str
93114
if not project_id:
94115
_, default_project_id = google.auth.default()
@@ -122,7 +143,7 @@ def _batch_write(self, series: List[TimeSeries]) -> None:
122143
time_series=series[
123144
write_ind : write_ind + MAX_BATCH_WRITE
124145
],
125-
)
146+
),
126147
)
127148
write_ind += MAX_BATCH_WRITE
128149

opentelemetry-exporter-gcp-monitoring/tests/fixtures/gcmfake.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515
from collections import defaultdict
1616
from concurrent.futures import ThreadPoolExecutor
1717
from dataclasses import dataclass
18-
from typing import Callable, Iterable, List, Mapping, Tuple, Type
18+
from functools import partial
19+
from typing import Callable, Iterable, List, Mapping, Tuple, Type, cast
20+
from unittest.mock import patch
1921

2022
import grpc
2123
import pytest
2224
from google.api.metric_pb2 import MetricDescriptor
2325
from google.cloud.monitoring_v3 import (
2426
CreateMetricDescriptorRequest,
2527
CreateTimeSeriesRequest,
26-
MetricServiceClient,
2728
)
2829
from google.cloud.monitoring_v3.services.metric_service.transports import (
2930
MetricServiceGrpcTransport,
@@ -34,8 +35,8 @@
3435
from google.protobuf.message import Message
3536
from grpc import (
3637
GenericRpcHandler,
37-
RpcContext,
3838
RpcMethodHandler,
39+
ServicerContext,
3940
insecure_channel,
4041
method_handlers_generic_handler,
4142
unary_unary_rpc_method_handler,
@@ -46,8 +47,14 @@
4647
from opentelemetry.sdk.metrics import MeterProvider
4748
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
4849

49-
# Mapping of fully qualified GCM API method names to list of requests received
50-
GcmCalls = Mapping[str, List[Message]]
50+
51+
@dataclass
52+
class GcmCall:
53+
message: Message
54+
user_agent: str
55+
56+
57+
GcmCalls = Mapping[str, List[GcmCall]]
5158

5259
PROJECT_ID = "fakeproject"
5360

@@ -89,12 +96,16 @@ def __init__(self):
8996
def _make_impl(
9097
self,
9198
method: str,
92-
behavior: Callable[[Message, RpcContext], Message],
99+
behavior: Callable[[Message, ServicerContext], Message],
93100
deserializer,
94101
serializer,
95102
) -> Tuple[str, RpcMethodHandler]:
96-
def impl(req: Message, context: RpcContext) -> Message:
97-
self._calls[f"/{self._service}/{method}"].append(req)
103+
def impl(req: Message, context: ServicerContext) -> Message:
104+
metadata_dict = dict(context.invocation_metadata())
105+
user_agent = cast(str, metadata_dict["user-agent"])
106+
self._calls[f"/{self._service}/{method}"].append(
107+
GcmCall(message=req, user_agent=user_agent)
108+
)
98109
return behavior(req, context)
99110

100111
return method, unary_unary_rpc_method_handler(
@@ -114,7 +125,7 @@ def get_calls(self) -> GcmCalls:
114125

115126
@dataclass
116127
class GcmFake:
117-
client: MetricServiceClient
128+
exporter: CloudMonitoringMetricsExporter
118129
get_calls: Callable[[], GcmCalls]
119130

120131

@@ -131,10 +142,17 @@ def fixture_gcmfake() -> Iterable[GcmFake]:
131142
server = grpc.server(executor, handlers=[handler])
132143
port = server.add_insecure_port("localhost:0")
133144
server.start()
134-
with insecure_channel(f"localhost:{port}") as channel:
145+
146+
# patch MetricServiceGrpcTransport.create_channel staticmethod to return an insecure
147+
# channel but otherwise respect any parameters passed to it
148+
with patch.object(
149+
MetricServiceGrpcTransport,
150+
"create_channel",
151+
partial(insecure_channel, target=f"localhost:{port}"),
152+
):
135153
yield GcmFake(
136-
client=MetricServiceClient(
137-
transport=MetricServiceGrpcTransport(channel=channel),
154+
exporter=CloudMonitoringMetricsExporter(
155+
project_id=PROJECT_ID
138156
),
139157
get_calls=handler.get_calls,
140158
)
@@ -161,11 +179,7 @@ def make_meter_provider(**kwargs) -> MeterProvider:
161179
mp = MeterProvider(
162180
**{
163181
"metric_readers": [
164-
PeriodicExportingMetricReader(
165-
CloudMonitoringMetricsExporter(
166-
project_id=PROJECT_ID, client=gcmfake.client
167-
)
168-
)
182+
PeriodicExportingMetricReader(gcmfake.exporter)
169183
],
170184
"shutdown_on_exit": False,
171185
**kwargs,

opentelemetry-exporter-gcp-monitoring/tests/fixtures/snapshot_gcmcalls.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,20 @@ def serialize(
4343
exclude: Optional[PropertyFilter] = None,
4444
matcher: Optional[PropertyMatcher] = None,
4545
) -> SerializedData:
46-
calls = cast(GcmCalls, data)
46+
gcmcalls = cast(GcmCalls, data)
4747
json = {}
48-
for method, requests in calls.items():
48+
for method, calls in gcmcalls.items():
4949
dict_requests = []
50-
for request in requests:
51-
if isinstance(request, proto.message.Message):
52-
request = type(request).pb(request)
53-
elif isinstance(request, google.protobuf.message.Message):
50+
for call in calls:
51+
if isinstance(call.message, proto.message.Message):
52+
call.message = type(call.message).pb(call.message)
53+
elif isinstance(call.message, google.protobuf.message.Message):
5454
pass
5555
else:
5656
raise ValueError(
57-
f"Excepted a proto-plus or protobuf message, got {type(request)}"
57+
f"Excepted a proto-plus or protobuf message, got {type(call)}"
5858
)
59-
dict_requests.append(json_format.MessageToDict(request))
59+
dict_requests.append(json_format.MessageToDict(call.message))
6060
json[method] = dict_requests
6161

6262
return super().serialize(json, exclude=exclude, matcher=matcher)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import re
16+
17+
from fixtures.gcmfake import GcmFake, GcmFakeMeterProvider
18+
19+
20+
def test_with_resource(
21+
gcmfake_meter_provider: GcmFakeMeterProvider,
22+
gcmfake: GcmFake,
23+
) -> None:
24+
meter_provider = gcmfake_meter_provider()
25+
counter = meter_provider.get_meter(__name__).create_counter(
26+
"mycounter", description="foo", unit="{myunit}"
27+
)
28+
counter.add(12)
29+
meter_provider.force_flush()
30+
31+
for calls in gcmfake.get_calls().values():
32+
for call in calls:
33+
assert (
34+
re.match(
35+
r"^opentelemetry-python \S+; google-cloud-metric-exporter \S+ grpc-python/\S+",
36+
call.user_agent,
37+
)
38+
is not None
39+
)

opentelemetry-exporter-gcp-trace/src/opentelemetry/exporter/cloud_trace/__init__.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
import pkg_resources
9191
from google.cloud.trace_v2 import BatchWriteSpansRequest, TraceServiceClient
9292
from google.cloud.trace_v2 import types as trace_types
93+
from google.cloud.trace_v2.services.trace_service.transports import (
94+
TraceServiceGrpcTransport,
95+
)
9396
from google.protobuf.timestamp_pb2 import ( # pylint: disable=no-name-in-module
9497
Timestamp,
9598
)
@@ -113,6 +116,18 @@
113116

114117
logger = logging.getLogger(__name__)
115118

119+
_OTEL_SDK_VERSION = pkg_resources.get_distribution("opentelemetry-sdk").version
120+
_USER_AGENT = f"opentelemetry-python {_OTEL_SDK_VERSION}; google-cloud-trace-exporter {__version__}"
121+
122+
# Set user-agent metadata, see https://github.com/grpc/grpc/issues/23644 and default options
123+
# from
124+
# https://github.com/googleapis/python-trace/blob/v1.7.3/google/cloud/trace_v1/services/trace_service/transports/grpc.py#L177-L180
125+
_OPTIONS = [
126+
("grpc.max_send_message_length", -1),
127+
("grpc.max_receive_message_length", -1),
128+
("grpc.primary_user_agent", _USER_AGENT),
129+
]
130+
116131
MAX_NUM_LINKS = 128
117132
MAX_NUM_EVENTS = 32
118133
MAX_EVENT_ATTRS = 4
@@ -141,7 +156,14 @@ def __init__(
141156
client=None,
142157
resource_regex=None,
143158
):
144-
self.client: TraceServiceClient = client or TraceServiceClient()
159+
self.client: TraceServiceClient = client or TraceServiceClient(
160+
transport=TraceServiceGrpcTransport(
161+
channel=TraceServiceGrpcTransport.create_channel(
162+
options=_OPTIONS
163+
)
164+
)
165+
)
166+
145167
if not project_id:
146168
project_id = environ.get(OTEL_EXPORTER_GCP_TRACE_PROJECT_ID)
147169
if not project_id:

opentelemetry-exporter-gcp-trace/tests/test_cloud_trace_exporter.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from unittest import mock
1818

1919
import pkg_resources
20+
from google.cloud.trace_v2.services.trace_service.transports import (
21+
TraceServiceGrpcTransport,
22+
)
2023
from google.cloud.trace_v2.types import AttributeValue, BatchWriteSpansRequest
2124
from google.cloud.trace_v2.types import Span as ProtoSpan
2225
from google.cloud.trace_v2.types import TruncatableString
@@ -51,13 +54,18 @@
5154
# pylint: disable=too-many-public-methods
5255
class TestCloudTraceSpanExporter(unittest.TestCase):
5356
def setUp(self):
54-
self.client_patcher = mock.patch(
55-
"opentelemetry.exporter.cloud_trace.TraceServiceClient"
56-
)
57-
self.client_patcher.start()
57+
self.patchers = [
58+
mock.patch(
59+
"opentelemetry.exporter.cloud_trace.TraceServiceClient"
60+
),
61+
mock.patch.object(TraceServiceGrpcTransport, "create_channel"),
62+
]
63+
for patcher in self.patchers:
64+
patcher.start()
5865

5966
def tearDown(self):
60-
self.client_patcher.stop()
67+
for patcher in self.patchers:
68+
patcher.stop()
6169

6270
@classmethod
6371
def setUpClass(cls):

0 commit comments

Comments
 (0)