langgraph-api 0.4.29__tar.gz → 0.4.30__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of langgraph-api might be problematic. Click here for more details.

Files changed (132) hide show
  1. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/PKG-INFO +4 -1
  2. langgraph_api-0.4.30/langgraph_api/__init__.py +1 -0
  3. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/meta.py +1 -3
  4. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/config.py +10 -0
  5. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/generated/core_api_pb2_grpc.py +1 -1
  6. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/http_metrics.py +15 -35
  7. langgraph_api-0.4.30/langgraph_api/http_metrics_utils.py +38 -0
  8. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/queue_entrypoint.py +1 -2
  9. langgraph_api-0.4.30/langgraph_api/self_hosted_metrics.py +380 -0
  10. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/stream.py +2 -0
  11. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/pyproject.toml +3 -0
  12. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/uv.lock +334 -147
  13. langgraph_api-0.4.29/langgraph_api/__init__.py +0 -1
  14. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/.gitignore +0 -0
  15. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/LICENSE +0 -0
  16. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/Makefile +0 -0
  17. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/README.md +0 -0
  18. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/.gitignore +0 -0
  19. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/Makefile +0 -0
  20. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/README.md +0 -0
  21. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/burst.js +0 -0
  22. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/clean.js +0 -0
  23. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/graphs.js +0 -0
  24. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/package.json +0 -0
  25. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/ramp.js +0 -0
  26. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/update-revision.js +0 -0
  27. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/weather.js +0 -0
  28. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/constraints.txt +0 -0
  29. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/forbidden.txt +0 -0
  30. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/healthcheck.py +0 -0
  31. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/__init__.py +0 -0
  32. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/a2a.py +0 -0
  33. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/assistants.py +0 -0
  34. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/mcp.py +0 -0
  35. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/openapi.py +0 -0
  36. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/runs.py +0 -0
  37. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/store.py +0 -0
  38. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/threads.py +0 -0
  39. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/ui.py +0 -0
  40. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/asgi_transport.py +0 -0
  41. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/asyncio.py +0 -0
  42. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/__init__.py +0 -0
  43. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/custom.py +0 -0
  44. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/langsmith/__init__.py +0 -0
  45. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/langsmith/backend.py +0 -0
  46. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/langsmith/client.py +0 -0
  47. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/middleware.py +0 -0
  48. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/noop.py +0 -0
  49. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/studio_user.py +0 -0
  50. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/cli.py +0 -0
  51. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/command.py +0 -0
  52. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/cron_scheduler.py +0 -0
  53. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/errors.py +0 -0
  54. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/executor_entrypoint.py +0 -0
  55. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/feature_flags.py +0 -0
  56. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/graph.py +0 -0
  57. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/__init__.py +0 -0
  58. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/client.py +0 -0
  59. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/generated/__init__.py +0 -0
  60. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/generated/core_api_pb2.py +0 -0
  61. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/generated/core_api_pb2.pyi +0 -0
  62. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/ops.py +0 -0
  63. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/scripts/generate_protos.sh +0 -0
  64. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/http.py +0 -0
  65. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/.gitignore +0 -0
  66. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/.prettierrc +0 -0
  67. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/__init__.py +0 -0
  68. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/base.py +0 -0
  69. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/build.mts +0 -0
  70. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/client.http.mts +0 -0
  71. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/client.mts +0 -0
  72. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/errors.py +0 -0
  73. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/global.d.ts +0 -0
  74. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/package.json +0 -0
  75. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/remote.py +0 -0
  76. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/schema.py +0 -0
  77. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/graph.mts +0 -0
  78. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/load.hooks.mjs +0 -0
  79. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/preload.mjs +0 -0
  80. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/utils/files.mts +0 -0
  81. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/utils/importMap.mts +0 -0
  82. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  83. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/utils/serde.mts +0 -0
  84. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/sse.py +0 -0
  85. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/traceblock.mts +0 -0
  86. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/tsconfig.json +0 -0
  87. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/ui.py +0 -0
  88. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/yarn.lock +0 -0
  89. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/logging.py +0 -0
  90. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/metadata.py +0 -0
  91. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/middleware/__init__.py +0 -0
  92. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/middleware/http_logger.py +0 -0
  93. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/middleware/private_network.py +0 -0
  94. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/middleware/request_id.py +0 -0
  95. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/models/__init__.py +0 -0
  96. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/models/run.py +0 -0
  97. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/patch.py +0 -0
  98. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/route.py +0 -0
  99. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/schema.py +0 -0
  100. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/serde.py +0 -0
  101. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/server.py +0 -0
  102. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/sse.py +0 -0
  103. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/state.py +0 -0
  104. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/store.py +0 -0
  105. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/thread_ttl.py +0 -0
  106. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/traceblock.py +0 -0
  107. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/tunneling/cloudflare.py +0 -0
  108. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/__init__.py +0 -0
  109. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/cache.py +0 -0
  110. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/config.py +0 -0
  111. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/future.py +0 -0
  112. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/headers.py +0 -0
  113. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/retriable_client.py +0 -0
  114. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/stream_codec.py +0 -0
  115. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/uuids.py +0 -0
  116. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/validation.py +0 -0
  117. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/webhook.py +0 -0
  118. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/worker.py +0 -0
  119. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_license/__init__.py +0 -0
  120. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_license/validation.py +0 -0
  121. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/__init__.py +0 -0
  122. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/checkpoint.py +0 -0
  123. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/database.py +0 -0
  124. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/lifespan.py +0 -0
  125. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/metrics.py +0 -0
  126. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/ops.py +0 -0
  127. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/queue.py +0 -0
  128. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/retry.py +0 -0
  129. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/store.py +0 -0
  130. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/logging.json +0 -0
  131. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/openapi.json +0 -0
  132. {langgraph_api-0.4.29 → langgraph_api-0.4.30}/scripts/create_license.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.4.29
3
+ Version: 0.4.30
4
4
  Author-email: Nuno Campos <nuno@langchain.dev>, Will Fu-Hinthorn <will@langchain.dev>
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -17,6 +17,9 @@ Requires-Dist: langgraph-runtime-inmem<0.15.0,>=0.14.0
17
17
  Requires-Dist: langgraph-sdk>=0.2.0
18
18
  Requires-Dist: langgraph>=0.4.0
19
19
  Requires-Dist: langsmith>=0.3.45
20
+ Requires-Dist: opentelemetry-api>=1.37.0
21
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.37.0
22
+ Requires-Dist: opentelemetry-sdk>=1.37.0
20
23
  Requires-Dist: orjson>=3.9.7
21
24
  Requires-Dist: protobuf<7.0.0,>=6.32.1
22
25
  Requires-Dist: pyjwt>=2.9.0
@@ -0,0 +1 @@
1
+ __version__ = "0.4.30"
@@ -1,5 +1,3 @@
1
- from typing import cast
2
-
3
1
  import langgraph.version
4
2
  import structlog
5
3
  from starlette.responses import JSONResponse, PlainTextResponse
@@ -48,7 +46,7 @@ async def meta_metrics(request: ApiRequest):
48
46
 
49
47
  # collect stats
50
48
  metrics = get_metrics()
51
- worker_metrics = cast(dict[str, int], metrics["workers"])
49
+ worker_metrics = metrics["workers"]
52
50
  workers_max = worker_metrics["max"]
53
51
  workers_active = worker_metrics["active"]
54
52
  workers_available = worker_metrics["available"]
@@ -374,6 +374,16 @@ API_VARIANT = env("LANGSMITH_LANGGRAPH_API_VARIANT", cast=str, default="")
374
374
 
375
375
  # UI
376
376
  UI_USE_BUNDLER = env("LANGGRAPH_UI_BUNDLER", cast=bool, default=False)
377
+
378
+ SELF_HOSTED_METRICS_ENABLED = env(
379
+ "SELF_HOSTED_METRICS_ENABLED", cast=bool, default=False
380
+ )
381
+ SELF_HOSTED_METRICS_ENDPOINT = env(
382
+ "SELF_HOSTED_METRICS_ENDPOINT", cast=str, default=None
383
+ )
384
+ SELF_HOSTED_METRICS_EXPORT_INTERVAL_MS = env(
385
+ "SELF_HOSTED_METRICS_EXPORT_INTERVAL_MS", cast=int, default=60000
386
+ )
377
387
  IS_QUEUE_ENTRYPOINT = False
378
388
  IS_EXECUTOR_ENTRYPOINT = False
379
389
  ref_sha = None
@@ -6,7 +6,7 @@ import warnings
6
6
  from . import core_api_pb2 as core__api__pb2
7
7
  from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
8
8
 
9
- GRPC_GENERATED_VERSION = '1.75.0'
9
+ GRPC_GENERATED_VERSION = '1.75.1'
10
10
  GRPC_VERSION = grpc.__version__
11
11
  _version_not_supported = False
12
12
 
@@ -1,51 +1,23 @@
1
1
  from collections import defaultdict
2
2
  from typing import Any
3
3
 
4
- FILTERED_ROUTES = {"/ok", "/info", "/metrics", "/docs", "/openapi.json"}
4
+ from langgraph_api import config
5
+ from langgraph_api.http_metrics_utils import (
6
+ HTTP_LATENCY_BUCKETS,
7
+ get_route,
8
+ should_filter_route,
9
+ )
5
10
 
6
11
  MAX_REQUEST_COUNT_ENTRIES = 5000
7
12
  MAX_HISTOGRAM_ENTRIES = 1000
8
13
 
9
14
 
10
- def get_route(route: Any) -> str | None:
11
- try:
12
- # default lg api routes use the custom APIRoute where scope["route"] is set to a string
13
- if isinstance(route, str):
14
- return route
15
- else:
16
- # custom FastAPI routes provided by user_router attach an object to scope["route"]
17
- route_path = getattr(route, "path", None)
18
- return route_path
19
- except Exception:
20
- return None
21
-
22
-
23
- def should_filter_route(route_path: str) -> bool:
24
- # use endswith to honor MOUNT_PREFIX
25
- return any(route_path.endswith(suffix) for suffix in FILTERED_ROUTES)
26
-
27
-
28
15
  class HTTPMetricsCollector:
29
16
  def __init__(self):
30
17
  # Counter: Key: (method, route, status), Value: count
31
18
  self._request_counts: dict[tuple[str, str, int], int] = defaultdict(int)
32
19
 
33
- self._histogram_buckets = [
34
- 0.01,
35
- 0.1,
36
- 0.5,
37
- 1,
38
- 5,
39
- 15,
40
- 30,
41
- 60,
42
- 120,
43
- 300,
44
- 600,
45
- 1800,
46
- 3600,
47
- float("inf"),
48
- ]
20
+ self._histogram_buckets = HTTP_LATENCY_BUCKETS
49
21
  self._histogram_bucket_labels = [
50
22
  "+Inf" if value == float("inf") else str(value)
51
23
  for value in self._histogram_buckets
@@ -97,6 +69,14 @@ class HTTPMetricsCollector:
97
69
  hist_data["sum"] += latency_seconds
98
70
  hist_data["count"] += 1
99
71
 
72
+ try:
73
+ if config.SELF_HOSTED_METRICS_ENABLED:
74
+ from langgraph_api.self_hosted_metrics import record_http_request
75
+
76
+ record_http_request(method, route_path, status, latency_seconds)
77
+ except Exception:
78
+ pass
79
+
100
80
  def get_metrics(
101
81
  self,
102
82
  project_id: str | None,
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+
3
+ FILTERED_ROUTES = {"/ok", "/info", "/metrics", "/docs", "/openapi.json"}
4
+
5
+ HTTP_LATENCY_BUCKETS = [
6
+ 0.01,
7
+ 0.1,
8
+ 0.5,
9
+ 1,
10
+ 5,
11
+ 15,
12
+ 30,
13
+ 60,
14
+ 120,
15
+ 300,
16
+ 600,
17
+ 1800,
18
+ 3600,
19
+ float("inf"),
20
+ ]
21
+
22
+
23
+ def get_route(route: Any) -> str | None:
24
+ try:
25
+ # default lg api routes use the custom APIRoute where scope["route"] is set to a string
26
+ if isinstance(route, str):
27
+ return route
28
+ else:
29
+ # custom FastAPI routes provided by user_router attach an object to scope["route"]
30
+ route_path = getattr(route, "path", None)
31
+ return route_path
32
+ except Exception:
33
+ return None
34
+
35
+
36
+ def should_filter_route(route_path: str) -> bool:
37
+ # use endswith to honor MOUNT_PREFIX
38
+ return any(route_path.endswith(suffix) for suffix in FILTERED_ROUTES)
@@ -18,7 +18,6 @@ import logging.config
18
18
  import pathlib
19
19
  import signal
20
20
  from contextlib import asynccontextmanager
21
- from typing import cast
22
21
 
23
22
  import structlog
24
23
 
@@ -50,7 +49,7 @@ async def health_and_metrics_server():
50
49
  metrics_format = "prometheus"
51
50
 
52
51
  metrics = get_metrics()
53
- worker_metrics = cast(dict[str, int], metrics["workers"])
52
+ worker_metrics = metrics["workers"]
54
53
  workers_max = worker_metrics["max"]
55
54
  workers_active = worker_metrics["active"]
56
55
  workers_available = worker_metrics["available"]
@@ -0,0 +1,380 @@
1
+ import os
2
+
3
+ import structlog
4
+ from opentelemetry import metrics
5
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
6
+ OTLPMetricExporter,
7
+ )
8
+ from opentelemetry.metrics import CallbackOptions, Observation
9
+ from opentelemetry.sdk.metrics import MeterProvider
10
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
11
+ from opentelemetry.sdk.resources import Resource
12
+
13
+ from langgraph_api import asyncio as lg_asyncio
14
+ from langgraph_api import config, metadata
15
+ from langgraph_api.http_metrics_utils import HTTP_LATENCY_BUCKETS
16
+ from langgraph_runtime.database import connect, pool_stats
17
+ from langgraph_runtime.metrics import get_metrics
18
+ from langgraph_runtime.ops import Runs
19
+
20
+ logger = structlog.stdlib.get_logger(__name__)
21
+
22
+ _meter_provider = None
23
+ _customer_attributes = {}
24
+
25
+ _http_request_counter = None
26
+ _http_latency_histogram = None
27
+
28
+
29
+ def initialize_self_hosted_metrics():
30
+ global \
31
+ _meter_provider, \
32
+ _http_request_counter, \
33
+ _http_latency_histogram, \
34
+ _customer_attributes
35
+
36
+ if not config.SELF_HOSTED_METRICS_ENABLED:
37
+ return
38
+
39
+ if not config.SELF_HOSTED_METRICS_ENDPOINT:
40
+ raise RuntimeError(
41
+ "SELF_HOSTED_METRICS_ENABLED is true but no SELF_HOSTED_METRICS_ENDPOINT is configured"
42
+ )
43
+
44
+ # for now, this is only enabled for fully self-hosted customers
45
+ # we will need to update the otel collector auth model to support hybrid customers
46
+ if not config.LANGGRAPH_CLOUD_LICENSE_KEY:
47
+ logger.warning(
48
+ "Self-hosted metrics require a license key, and do not work with hybrid deployments yet."
49
+ )
50
+ return
51
+
52
+ try:
53
+ exporter = OTLPMetricExporter(
54
+ endpoint=config.SELF_HOSTED_METRICS_ENDPOINT,
55
+ headers={"X-Langchain-License-Key": config.LANGGRAPH_CLOUD_LICENSE_KEY},
56
+ )
57
+
58
+ # this will periodically export metrics to our beacon lgp otel collector in a separate thread
59
+ metric_reader = PeriodicExportingMetricReader(
60
+ exporter=exporter,
61
+ export_interval_millis=config.SELF_HOSTED_METRICS_EXPORT_INTERVAL_MS,
62
+ )
63
+
64
+ resource_attributes = {
65
+ "service.name": "LGP_Self_Hosted",
66
+ }
67
+
68
+ resource = Resource.create(resource_attributes)
69
+
70
+ if config.LANGGRAPH_CLOUD_LICENSE_KEY:
71
+ try:
72
+ from langgraph_license.validation import (
73
+ CUSTOMER_ID, # type: ignore[unresolved-import]
74
+ CUSTOMER_NAME, # type: ignore[unresolved-import]
75
+ )
76
+
77
+ if CUSTOMER_ID:
78
+ _customer_attributes["customer_id"] = CUSTOMER_ID
79
+ if CUSTOMER_NAME:
80
+ _customer_attributes["customer_name"] = CUSTOMER_NAME
81
+ except ImportError:
82
+ pass
83
+ except Exception as e:
84
+ logger.warning("Failed to get customer info from license", exc_info=e)
85
+
86
+ # resolves to pod name in k8s, or container id in docker
87
+ instance_id = os.environ.get("HOSTNAME")
88
+ if instance_id:
89
+ _customer_attributes["instance_id"] = instance_id
90
+
91
+ _meter_provider = MeterProvider(
92
+ metric_readers=[metric_reader], resource=resource
93
+ )
94
+ metrics.set_meter_provider(_meter_provider)
95
+
96
+ meter = metrics.get_meter("langgraph_api.self_hosted")
97
+
98
+ meter.create_observable_gauge(
99
+ name="lg_api_num_pending_runs",
100
+ description="The number of runs currently pending",
101
+ unit="1",
102
+ callbacks=[_get_pending_runs_callback],
103
+ )
104
+
105
+ meter.create_observable_gauge(
106
+ name="lg_api_num_running_runs",
107
+ description="The number of runs currently running",
108
+ unit="1",
109
+ callbacks=[_get_running_runs_callback],
110
+ )
111
+
112
+ if config.N_JOBS_PER_WORKER > 0:
113
+ meter.create_observable_gauge(
114
+ name="lg_api_workers_max",
115
+ description="The maximum number of workers available",
116
+ unit="1",
117
+ callbacks=[_get_workers_max_callback],
118
+ )
119
+
120
+ meter.create_observable_gauge(
121
+ name="lg_api_workers_active",
122
+ description="The number of currently active workers",
123
+ unit="1",
124
+ callbacks=[_get_workers_active_callback],
125
+ )
126
+
127
+ meter.create_observable_gauge(
128
+ name="lg_api_workers_available",
129
+ description="The number of available (idle) workers",
130
+ unit="1",
131
+ callbacks=[_get_workers_available_callback],
132
+ )
133
+
134
+ if not config.IS_QUEUE_ENTRYPOINT and not config.IS_EXECUTOR_ENTRYPOINT:
135
+ _http_request_counter = meter.create_counter(
136
+ name="lg_api_http_requests_total",
137
+ description="Total number of HTTP requests",
138
+ unit="1",
139
+ )
140
+
141
+ _http_latency_histogram = meter.create_histogram(
142
+ name="lg_api_http_requests_latency_seconds",
143
+ description="HTTP request latency in seconds",
144
+ unit="s",
145
+ explicit_bucket_boundaries_advisory=[
146
+ b for b in HTTP_LATENCY_BUCKETS if b != float("inf")
147
+ ],
148
+ )
149
+
150
+ meter.create_observable_gauge(
151
+ name="lg_api_pg_pool_max",
152
+ description="The maximum size of the postgres connection pool",
153
+ unit="1",
154
+ callbacks=[_get_pg_pool_max_callback],
155
+ )
156
+
157
+ meter.create_observable_gauge(
158
+ name="lg_api_pg_pool_size",
159
+ description="Number of connections currently managed by the postgres connection pool",
160
+ unit="1",
161
+ callbacks=[_get_pg_pool_size_callback],
162
+ )
163
+
164
+ meter.create_observable_gauge(
165
+ name="lg_api_pg_pool_available",
166
+ description="Number of connections currently idle in the postgres connection pool",
167
+ unit="1",
168
+ callbacks=[_get_pg_pool_available_callback],
169
+ )
170
+
171
+ meter.create_observable_gauge(
172
+ name="lg_api_redis_pool_max",
173
+ description="The maximum size of the redis connection pool",
174
+ unit="1",
175
+ callbacks=[_get_redis_pool_max_callback],
176
+ )
177
+
178
+ meter.create_observable_gauge(
179
+ name="lg_api_redis_pool_size",
180
+ description="Number of connections currently in use in the redis connection pool",
181
+ unit="1",
182
+ callbacks=[_get_redis_pool_size_callback],
183
+ )
184
+
185
+ meter.create_observable_gauge(
186
+ name="lg_api_redis_pool_available",
187
+ description="Number of connections currently idle in the redis connection pool",
188
+ unit="1",
189
+ callbacks=[_get_redis_pool_available_callback],
190
+ )
191
+
192
+ logger.info(
193
+ "Self-hosted metrics initialized successfully",
194
+ endpoint=config.SELF_HOSTED_METRICS_ENDPOINT,
195
+ export_interval_ms=config.SELF_HOSTED_METRICS_EXPORT_INTERVAL_MS,
196
+ )
197
+
198
+ except Exception as e:
199
+ logger.exception("Failed to initialize self-hosted metrics", exc_info=e)
200
+
201
+
202
+ def shutdown_self_hosted_metrics():
203
+ global _meter_provider
204
+
205
+ if _meter_provider:
206
+ try:
207
+ logger.info("Shutting down self-hosted metrics")
208
+ _meter_provider.shutdown(timeout_millis=5000)
209
+ _meter_provider = None
210
+ except Exception as e:
211
+ logger.exception("Failed to shutdown self-hosted metrics", exc_info=e)
212
+
213
+
214
+ def record_http_request(
215
+ method: str, route_path: str, status: int, latency_seconds: float
216
+ ):
217
+ if not _meter_provider or not _http_request_counter or not _http_latency_histogram:
218
+ return
219
+
220
+ attributes = {"method": method, "path": route_path, "status": str(status)}
221
+ if _customer_attributes:
222
+ attributes.update(_customer_attributes)
223
+
224
+ _http_request_counter.add(1, attributes)
225
+ _http_latency_histogram.record(latency_seconds, attributes)
226
+
227
+
228
+ def _get_queue_stats():
229
+ async def _fetch_queue_stats():
230
+ try:
231
+ async with connect() as conn:
232
+ return await Runs.stats(conn)
233
+ except Exception as e:
234
+ logger.warning("Failed to get queue stats from database", exc_info=e)
235
+ return {"n_pending": 0, "n_running": 0}
236
+
237
+ try:
238
+ future = lg_asyncio.run_coroutine_threadsafe(_fetch_queue_stats())
239
+ return future.result(timeout=5)
240
+ except Exception as e:
241
+ logger.warning("Failed to get queue stats", exc_info=e)
242
+ return {"n_pending": 0, "n_running": 0}
243
+
244
+
245
+ def _get_pool_stats():
246
+ # _get_pool() inside the pool_stats fn will not work correctly if called from the daemon thread created by PeriodicExportingMetricReader,
247
+ # so we submit this as a coro to run in the main event loop
248
+ async def _fetch_pool_stats():
249
+ try:
250
+ return pool_stats(
251
+ metadata.PROJECT_ID, metadata.HOST_REVISION_ID, format="json"
252
+ )
253
+ except Exception as e:
254
+ logger.warning("Failed to get pool stats", exc_info=e)
255
+ return {"postgres": {}, "redis": {}}
256
+
257
+ try:
258
+ future = lg_asyncio.run_coroutine_threadsafe(_fetch_pool_stats())
259
+ return future.result(timeout=5)
260
+ except Exception as e:
261
+ logger.warning("Failed to get pool stats", exc_info=e)
262
+ return {"postgres": {}, "redis": {}}
263
+
264
+
265
+ def _get_pending_runs_callback(options: CallbackOptions):
266
+ try:
267
+ stats = _get_queue_stats()
268
+ return [Observation(stats.get("n_pending", 0), attributes=_customer_attributes)]
269
+ except Exception as e:
270
+ logger.warning("Failed to get pending runs", exc_info=e)
271
+ return [Observation(0, attributes=_customer_attributes)]
272
+
273
+
274
+ def _get_running_runs_callback(options: CallbackOptions):
275
+ try:
276
+ stats = _get_queue_stats()
277
+ return [Observation(stats.get("n_running", 0), attributes=_customer_attributes)]
278
+ except Exception as e:
279
+ logger.warning("Failed to get running runs", exc_info=e)
280
+ return [Observation(0, attributes=_customer_attributes)]
281
+
282
+
283
+ def _get_workers_max_callback(options: CallbackOptions):
284
+ try:
285
+ metrics_data = get_metrics()
286
+ worker_metrics = metrics_data.get("workers", {})
287
+ return [
288
+ Observation(worker_metrics.get("max", 0), attributes=_customer_attributes)
289
+ ]
290
+ except Exception as e:
291
+ logger.warning("Failed to get max workers", exc_info=e)
292
+ return [Observation(0, attributes=_customer_attributes)]
293
+
294
+
295
+ def _get_workers_active_callback(options: CallbackOptions):
296
+ try:
297
+ metrics_data = get_metrics()
298
+ worker_metrics = metrics_data.get("workers", {})
299
+ return [
300
+ Observation(
301
+ worker_metrics.get("active", 0), attributes=_customer_attributes
302
+ )
303
+ ]
304
+ except Exception as e:
305
+ logger.warning("Failed to get active workers", exc_info=e)
306
+ return [Observation(0, attributes=_customer_attributes)]
307
+
308
+
309
+ def _get_workers_available_callback(options: CallbackOptions):
310
+ try:
311
+ metrics_data = get_metrics()
312
+ worker_metrics = metrics_data.get("workers", {})
313
+ return [
314
+ Observation(
315
+ worker_metrics.get("available", 0), attributes=_customer_attributes
316
+ )
317
+ ]
318
+ except Exception as e:
319
+ logger.warning("Failed to get available workers", exc_info=e)
320
+ return [Observation(0, attributes=_customer_attributes)]
321
+
322
+
323
+ def _get_pg_pool_max_callback(options: CallbackOptions):
324
+ try:
325
+ stats = _get_pool_stats()
326
+ pg_max = stats.get("postgres", {}).get("pool_max", 0)
327
+ return [Observation(pg_max, attributes=_customer_attributes)]
328
+ except Exception as e:
329
+ logger.warning("Failed to get PG pool max", exc_info=e)
330
+ return [Observation(0, attributes=_customer_attributes)]
331
+
332
+
333
+ def _get_pg_pool_size_callback(options: CallbackOptions):
334
+ try:
335
+ stats = _get_pool_stats()
336
+ pg_size = stats.get("postgres", {}).get("pool_size", 0)
337
+ return [Observation(pg_size, attributes=_customer_attributes)]
338
+ except Exception as e:
339
+ logger.warning("Failed to get PG pool size", exc_info=e)
340
+ return [Observation(0, attributes=_customer_attributes)]
341
+
342
+
343
+ def _get_pg_pool_available_callback(options: CallbackOptions):
344
+ try:
345
+ stats = _get_pool_stats()
346
+ pg_available = stats.get("postgres", {}).get("pool_available", 0)
347
+ return [Observation(pg_available, attributes=_customer_attributes)]
348
+ except Exception as e:
349
+ logger.warning("Failed to get PG pool available", exc_info=e)
350
+ return [Observation(0, attributes=_customer_attributes)]
351
+
352
+
353
+ def _get_redis_pool_max_callback(options: CallbackOptions):
354
+ try:
355
+ stats = _get_pool_stats()
356
+ redis_max = stats.get("redis", {}).get("max_connections", 0)
357
+ return [Observation(redis_max, attributes=_customer_attributes)]
358
+ except Exception as e:
359
+ logger.warning("Failed to get Redis pool max", exc_info=e)
360
+ return [Observation(0, attributes=_customer_attributes)]
361
+
362
+
363
+ def _get_redis_pool_size_callback(options: CallbackOptions):
364
+ try:
365
+ stats = _get_pool_stats()
366
+ redis_size = stats.get("redis", {}).get("in_use_connections", 0)
367
+ return [Observation(redis_size, attributes=_customer_attributes)]
368
+ except Exception as e:
369
+ logger.warning("Failed to get Redis pool size", exc_info=e)
370
+ return [Observation(0, attributes=_customer_attributes)]
371
+
372
+
373
+ def _get_redis_pool_available_callback(options: CallbackOptions):
374
+ try:
375
+ stats = _get_pool_stats()
376
+ redis_available = stats.get("redis", {}).get("idle_connections", 0)
377
+ return [Observation(redis_available, attributes=_customer_attributes)]
378
+ except Exception as e:
379
+ logger.warning("Failed to get Redis pool available", exc_info=e)
380
+ return [Observation(0, attributes=_customer_attributes)]
@@ -231,6 +231,8 @@ async def astream_state(
231
231
 
232
232
  # stream run
233
233
  if use_astream_events:
234
+ if USE_RUNTIME_CONTEXT_API:
235
+ kwargs["context"] = context
234
236
  async with (
235
237
  stack,
236
238
  aclosing( # type: ignore[invalid-argument-type]
@@ -36,6 +36,9 @@ dependencies = [
36
36
  "protobuf>=6.32.1,<7.0.0",
37
37
  "grpcio>=1.75.0,<2.0.0",
38
38
  "grpcio-tools>=1.75.0,<2.0.0",
39
+ "opentelemetry-api>=1.37.0",
40
+ "opentelemetry-sdk>=1.37.0",
41
+ "opentelemetry-exporter-otlp-proto-http>=1.37.0",
39
42
  ]
40
43
 
41
44
  [project.scripts]