Skip to content

Commit 1e3fd4d

Browse files
committed
Add metrics to instrumentation
1 parent 16eaec8 commit 1e3fd4d

File tree

8 files changed

+550
-5
lines changed

8 files changed

+550
-5
lines changed

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@
4949
from opentelemetry.instrumentation.openai_v2.package import _instruments
5050
from opentelemetry.instrumentation.openai_v2.utils import is_content_enabled
5151
from opentelemetry.instrumentation.utils import unwrap
52+
from opentelemetry.metrics import get_meter
5253
from opentelemetry.semconv.schemas import Schemas
5354
from opentelemetry.trace import get_tracer
5455

56+
from .meters import Meters
5557
from .patch import async_chat_completions_create, chat_completions_create
5658

5759

@@ -75,20 +77,29 @@ def _instrument(self, **kwargs):
7577
schema_url=Schemas.V1_28_0.value,
7678
event_logger_provider=event_logger_provider,
7779
)
80+
meter_provider = kwargs.get("meter_provider")
81+
self._meter = get_meter(
82+
__name__,
83+
"",
84+
meter_provider,
85+
schema_url=Schemas.V1_28_0.value,
86+
)
87+
88+
meters = Meters(self._meter)
7889

7990
wrap_function_wrapper(
8091
module="openai.resources.chat.completions",
8192
name="Completions.create",
8293
wrapper=chat_completions_create(
83-
tracer, event_logger, is_content_enabled()
94+
tracer, event_logger, meters, is_content_enabled()
8495
),
8596
)
8697

8798
wrap_function_wrapper(
8899
module="openai.resources.chat.completions",
89100
name="AsyncCompletions.create",
90101
wrapper=async_chat_completions_create(
91-
tracer, event_logger, is_content_enabled()
102+
tracer, event_logger, meters, is_content_enabled()
92103
),
93104
)
94105

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Meters:
2+
def __init__(self, meter):
3+
self.operation_duration_histogram = meter.create_histogram(
4+
name="gen_ai.client.operation.duration",
5+
description="Duration of gen_ai client operations",
6+
unit="seconds",
7+
)
8+
self.token_usage_histogram = meter.create_histogram(
9+
name="gen_ai.client.token.usage",
10+
description="Token usage of gen_ai client operations",
11+
unit="tokens",
12+
)

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515

16+
from timeit import default_timer
1617
from typing import Optional
1718

1819
from openai import Stream
@@ -23,6 +24,7 @@
2324
)
2425
from opentelemetry.trace import Span, SpanKind, Tracer
2526

27+
from .meters import Meters # Import the Meters class
2628
from .utils import (
2729
choice_to_event,
2830
get_llm_request_attributes,
@@ -34,7 +36,10 @@
3436

3537

3638
def chat_completions_create(
37-
tracer: Tracer, event_logger: EventLogger, capture_content: bool
39+
tracer: Tracer,
40+
event_logger: EventLogger,
41+
meters: Meters,
42+
capture_content: bool,
3843
):
3944
"""Wrap the `create` method of the `ChatCompletion` class to trace it."""
4045

@@ -54,6 +59,8 @@ def traced_method(wrapped, instance, args, kwargs):
5459
message_to_event(message, capture_content)
5560
)
5661

62+
start = default_timer()
63+
result = None
5764
try:
5865
result = wrapped(*args, **kwargs)
5966
if is_streaming(kwargs):
@@ -71,12 +78,23 @@ def traced_method(wrapped, instance, args, kwargs):
7178
except Exception as error:
7279
handle_span_exception(span, error)
7380
raise
81+
finally:
82+
duration = max((default_timer() - start), 0)
83+
_record_metrics(
84+
meters,
85+
duration,
86+
result,
87+
span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL],
88+
)
7489

7590
return traced_method
7691

7792

7893
def async_chat_completions_create(
79-
tracer: Tracer, event_logger: EventLogger, capture_content: bool
94+
tracer: Tracer,
95+
event_logger: EventLogger,
96+
meters: Meters,
97+
capture_content: bool,
8098
):
8199
"""Wrap the `create` method of the `AsyncChatCompletion` class to trace it."""
82100

@@ -96,6 +114,8 @@ async def traced_method(wrapped, instance, args, kwargs):
96114
message_to_event(message, capture_content)
97115
)
98116

117+
start = default_timer()
118+
result = None
99119
try:
100120
result = await wrapped(*args, **kwargs)
101121
if is_streaming(kwargs):
@@ -113,10 +133,55 @@ async def traced_method(wrapped, instance, args, kwargs):
113133
except Exception as error:
114134
handle_span_exception(span, error)
115135
raise
136+
finally:
137+
duration = max((default_timer() - start), 0)
138+
_record_metrics(
139+
meters,
140+
duration,
141+
result,
142+
span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL],
143+
)
116144

117145
return traced_method
118146

119147

148+
def _record_metrics(
149+
meters: Meters, duration: float, result, request_model: str
150+
):
151+
common_attributes = {
152+
GenAIAttributes.GEN_AI_OPERATION_NAME: GenAIAttributes.GenAiOperationNameValues.CHAT.value,
153+
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value,
154+
GenAIAttributes.GEN_AI_REQUEST_MODEL: request_model,
155+
}
156+
157+
if result and getattr(result, "model", None):
158+
common_attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL] = result.model
159+
160+
meters.operation_duration_histogram.record(
161+
duration,
162+
attributes=common_attributes,
163+
)
164+
165+
if result and getattr(result, "usage", None):
166+
input_attributes = {
167+
**common_attributes,
168+
GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.INPUT.value,
169+
}
170+
meters.token_usage_histogram.record(
171+
result.usage.prompt_tokens,
172+
attributes=input_attributes,
173+
)
174+
175+
completion_attributes = {
176+
**common_attributes,
177+
GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value,
178+
}
179+
meters.token_usage_histogram.record(
180+
result.usage.completion_tokens,
181+
attributes=completion_attributes,
182+
)
183+
184+
120185
def _set_response_attributes(
121186
span, result, event_logger: EventLogger, capture_content: bool
122187
):
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
interactions:
2+
- request:
3+
body: |-
4+
{
5+
"messages": [
6+
{
7+
"role": "user",
8+
"content": "Say this is a test"
9+
}
10+
],
11+
"model": "gpt-4o-mini",
12+
"stream": false
13+
}
14+
headers:
15+
accept:
16+
- application/json
17+
accept-encoding:
18+
- gzip, deflate
19+
authorization:
20+
- Bearer test_openai_api_key
21+
connection:
22+
- keep-alive
23+
content-length:
24+
- '106'
25+
content-type:
26+
- application/json
27+
host:
28+
- api.openai.com
29+
user-agent:
30+
- AsyncOpenAI/Python 1.26.0
31+
x-stainless-arch:
32+
- arm64
33+
x-stainless-async:
34+
- async:asyncio
35+
x-stainless-lang:
36+
- python
37+
x-stainless-os:
38+
- MacOS
39+
x-stainless-package-version:
40+
- 1.26.0
41+
x-stainless-runtime:
42+
- CPython
43+
x-stainless-runtime-version:
44+
- 3.12.5
45+
method: POST
46+
uri: https://api.openai.com/v1/chat/completions
47+
response:
48+
body:
49+
string: |-
50+
{
51+
"id": "chatcmpl-ASv9R2E7Yhb2e7bj4Xl0qm9s3J42Y",
52+
"object": "chat.completion",
53+
"created": 1731456237,
54+
"model": "gpt-4o-mini-2024-07-18",
55+
"choices": [
56+
{
57+
"index": 0,
58+
"message": {
59+
"role": "assistant",
60+
"content": "This is a test. How can I assist you further?",
61+
"refusal": null
62+
},
63+
"logprobs": null,
64+
"finish_reason": "stop"
65+
}
66+
],
67+
"usage": {
68+
"prompt_tokens": 12,
69+
"completion_tokens": 12,
70+
"total_tokens": 24,
71+
"prompt_tokens_details": {
72+
"cached_tokens": 0,
73+
"audio_tokens": 0
74+
},
75+
"completion_tokens_details": {
76+
"reasoning_tokens": 0,
77+
"audio_tokens": 0,
78+
"accepted_prediction_tokens": 0,
79+
"rejected_prediction_tokens": 0
80+
}
81+
},
82+
"system_fingerprint": "fp_0ba0d124f1"
83+
}
84+
headers:
85+
CF-Cache-Status:
86+
- DYNAMIC
87+
CF-RAY:
88+
- 8e1a80679a8311a6-MRS
89+
Connection:
90+
- keep-alive
91+
Content-Type:
92+
- application/json
93+
Date:
94+
- Wed, 13 Nov 2024 00:03:58 GMT
95+
Server:
96+
- cloudflare
97+
Set-Cookie: test_set_cookie
98+
Transfer-Encoding:
99+
- chunked
100+
X-Content-Type-Options:
101+
- nosniff
102+
access-control-expose-headers:
103+
- X-Request-ID
104+
alt-svc:
105+
- h3=":443"; ma=86400
106+
content-length:
107+
- '796'
108+
openai-organization: test_openai_org_id
109+
openai-processing-ms:
110+
- '359'
111+
openai-version:
112+
- '2020-10-01'
113+
strict-transport-security:
114+
- max-age=31536000; includeSubDomains; preload
115+
x-ratelimit-limit-requests:
116+
- '30000'
117+
x-ratelimit-limit-tokens:
118+
- '150000000'
119+
x-ratelimit-remaining-requests:
120+
- '29999'
121+
x-ratelimit-remaining-tokens:
122+
- '149999978'
123+
x-ratelimit-reset-requests:
124+
- 2ms
125+
x-ratelimit-reset-tokens:
126+
- 0s
127+
x-request-id:
128+
- req_41ea134c1fc450d4ca4cf8d0c6a7c53a
129+
status:
130+
code: 200
131+
message: OK
132+
version: 1

0 commit comments

Comments
 (0)