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.
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/PKG-INFO +4 -1
- langgraph_api-0.4.30/langgraph_api/__init__.py +1 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/meta.py +1 -3
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/config.py +10 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/generated/core_api_pb2_grpc.py +1 -1
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/http_metrics.py +15 -35
- langgraph_api-0.4.30/langgraph_api/http_metrics_utils.py +38 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/queue_entrypoint.py +1 -2
- langgraph_api-0.4.30/langgraph_api/self_hosted_metrics.py +380 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/stream.py +2 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/pyproject.toml +3 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/uv.lock +334 -147
- langgraph_api-0.4.29/langgraph_api/__init__.py +0 -1
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/.gitignore +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/LICENSE +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/Makefile +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/README.md +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/.gitignore +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/Makefile +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/README.md +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/burst.js +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/clean.js +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/graphs.js +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/package.json +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/ramp.js +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/update-revision.js +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/benchmark/weather.js +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/constraints.txt +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/forbidden.txt +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/healthcheck.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/a2a.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/assistants.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/mcp.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/openapi.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/runs.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/api/ui.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/asgi_transport.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/asyncio.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/custom.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/langsmith/backend.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/cli.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/command.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/executor_entrypoint.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/feature_flags.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/graph.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/client.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/generated/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/generated/core_api_pb2.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/generated/core_api_pb2.pyi +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/ops.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/scripts/generate_protos.sh +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/http.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/.prettierrc +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/build.mts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/client.http.mts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/client.mts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/package.json +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/remote.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/load.hooks.mjs +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/preload.mjs +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/utils/files.mts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/sse.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/traceblock.mts +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/tsconfig.json +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/ui.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/js/yarn.lock +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/logging.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/metadata.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/middleware/request_id.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/models/run.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/patch.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/route.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/serde.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/server.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/state.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/store.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/thread_ttl.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/traceblock.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/tunneling/cloudflare.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/cache.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/config.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/future.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/headers.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/retriable_client.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/stream_codec.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/utils/uuids.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/webhook.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/worker.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/__init__.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/checkpoint.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/database.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/lifespan.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/metrics.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/ops.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/queue.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/retry.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_runtime/store.py +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/logging.json +0 -0
- {langgraph_api-0.4.29 → langgraph_api-0.4.30}/openapi.json +0 -0
- {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.
|
|
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 =
|
|
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
|
{langgraph_api-0.4.29 → langgraph_api-0.4.30}/langgraph_api/grpc_ops/generated/core_api_pb2_grpc.py
RENAMED
|
@@ -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.
|
|
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
|
-
|
|
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 =
|
|
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)]
|