Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion databricks-mcp-server/databricks_mcp_server/tools/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from typing import Any, Dict, List, Optional

from databricks_tools_core.serving import (
get_serving_endpoint_build_logs as _get_serving_endpoint_build_logs,
get_serving_endpoint_server_logs as _get_serving_endpoint_server_logs,
get_serving_endpoint_status as _get_serving_endpoint_status,
query_serving_endpoint as _query_serving_endpoint,
list_serving_endpoints as _list_serving_endpoints,
query_serving_endpoint as _query_serving_endpoint,
)

from ..server import mcp
Expand Down Expand Up @@ -129,3 +131,61 @@ def list_serving_endpoints(limit: int = 50) -> List[Dict[str, Any]]:
]
"""
return _list_serving_endpoints(limit=limit)


@mcp.tool(timeout=30)
def get_serving_endpoint_build_logs(
name: str,
served_model_name: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get build logs for a served model in an endpoint.

Build logs show container image creation, dependency installation, and
model download. Use this to debug failed or stuck deployments.

Args:
name: Name of the serving endpoint
served_model_name: Name of the served model. If omitted, auto-resolved
from the endpoint config.

Returns:
Dictionary with:
- name: Endpoint name
- served_model_name: Resolved served model name
- logs: Build log text

Example:
>>> get_serving_endpoint_build_logs("my-endpoint")
{"name": "my-endpoint", "served_model_name": "model-1", "logs": "..."}
"""
return _get_serving_endpoint_build_logs(name=name, served_model_name=served_model_name)


@mcp.tool(timeout=30)
def get_serving_endpoint_server_logs(
name: str,
served_model_name: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get runtime server logs for a served model in an endpoint.

Server logs show recent stdout/stderr from the model server, including
inference processing and errors. Use this to debug prediction failures.

Args:
name: Name of the serving endpoint
served_model_name: Name of the served model. If omitted, auto-resolved
from the endpoint config.

Returns:
Dictionary with:
- name: Endpoint name
- served_model_name: Resolved served model name
- logs: Recent server log lines

Example:
>>> get_serving_endpoint_server_logs("my-endpoint")
{"name": "my-endpoint", "served_model_name": "model-1", "logs": "..."}
"""
return _get_serving_endpoint_server_logs(name=name, served_model_name=served_model_name)
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
"""

from .endpoints import (
get_serving_endpoint_build_logs,
get_serving_endpoint_server_logs,
get_serving_endpoint_status,
query_serving_endpoint,
list_serving_endpoints,
query_serving_endpoint,
)

__all__ = [
"get_serving_endpoint_build_logs",
"get_serving_endpoint_server_logs",
"get_serving_endpoint_status",
"query_serving_endpoint",
"list_serving_endpoints",
"query_serving_endpoint",
]
115 changes: 115 additions & 0 deletions databricks-tools-core/databricks_tools_core/serving/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
from typing import Any, Dict, List, Optional

from databricks.sdk.errors import NotFound, ResourceDoesNotExist
from databricks.sdk.service.serving import ChatMessage

from ..auth import get_workspace_client
Expand Down Expand Up @@ -248,3 +249,117 @@ def list_serving_endpoints(limit: Optional[int] = 50) -> List[Dict[str, Any]]:
)

return result


def _resolve_served_model_name(client: Any, name: str, served_model_name: Optional[str]) -> str:
"""
Resolve served_model_name from endpoint config if not provided.

Args:
client: Workspace client.
name: Endpoint name.
served_model_name: Explicit served model name, or None to auto-resolve.

Returns:
The resolved served model name.

Raises:
Exception: If endpoint not found or has no served entities.
"""
if served_model_name:
return served_model_name

try:
endpoint = client.serving_endpoints.get(name=name)
except (ResourceDoesNotExist, NotFound):
raise Exception(f"Endpoint '{name}' not found.")

if endpoint.config and endpoint.config.served_entities:
resolved = endpoint.config.served_entities[0].name
if resolved:
return resolved

raise Exception(f"Endpoint '{name}' has no served entities. Provide served_model_name explicitly.")


def get_serving_endpoint_build_logs(
name: str,
served_model_name: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get build logs for a served model in an endpoint.

Build logs contain container image creation output, dependency installation,
and model download steps. Use this to debug failed or stuck deployments.

Args:
name: Name of the serving endpoint.
served_model_name: Name of the served model. If omitted, auto-resolved
from the first served entity in the endpoint config.

Returns:
Dictionary with:
- name: Endpoint name
- served_model_name: Resolved served model name
- logs: Build log text (may be empty if no build has run)
"""
client = get_workspace_client()
resolved_name = _resolve_served_model_name(client, name, served_model_name)

try:
response = client.serving_endpoints.build_logs(
name=name,
served_model_name=resolved_name,
)
return {
"name": name,
"served_model_name": resolved_name,
"logs": response.logs or "",
}
except (ResourceDoesNotExist, NotFound):
raise Exception(
f"Served model '{resolved_name}' not found on endpoint '{name}'. "
"Check the served model name with get_serving_endpoint_status()."
)


def get_serving_endpoint_server_logs(
name: str,
served_model_name: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get runtime server logs for a served model in an endpoint.

Server logs contain recent stdout/stderr from the model server, including
inference request processing and application-level logging. Use this to
debug prediction errors or unexpected model behavior.

Args:
name: Name of the serving endpoint.
served_model_name: Name of the served model. If omitted, auto-resolved
from the first served entity in the endpoint config.

Returns:
Dictionary with:
- name: Endpoint name
- served_model_name: Resolved served model name
- logs: Recent server log lines (may be empty if no requests processed)
"""
client = get_workspace_client()
resolved_name = _resolve_served_model_name(client, name, served_model_name)

try:
response = client.serving_endpoints.logs(
name=name,
served_model_name=resolved_name,
)
return {
"name": name,
"served_model_name": resolved_name,
"logs": response.logs or "",
}
except (ResourceDoesNotExist, NotFound):
raise Exception(
f"Served model '{resolved_name}' not found on endpoint '{name}'. "
"Check the served model name with get_serving_endpoint_status()."
)
134 changes: 134 additions & 0 deletions databricks-tools-core/tests/unit/test_serving_logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Unit tests for Model Serving log retrieval functions."""

from unittest import mock

import pytest
from databricks.sdk.errors import ResourceDoesNotExist

from databricks_tools_core.serving import (
get_serving_endpoint_build_logs,
get_serving_endpoint_server_logs,
)

_GET_CLIENT = "databricks_tools_core.serving.endpoints.get_workspace_client"


def _mock_endpoint(name="test-ep", served_entities=None):
"""Build a mock endpoint object."""
ep = mock.Mock()
ep.name = name

if served_entities is None:
entity = mock.Mock()
entity.name = "model-1"
entity.entity_name = "main.ml.model"
entity.entity_version = "1"
served_entities = [entity]

config = mock.Mock()
config.served_entities = served_entities
ep.config = config

return ep


class TestGetServingEndpointBuildLogs:
"""Tests for get_serving_endpoint_build_logs."""

@mock.patch(_GET_CLIENT)
def test_build_logs_with_explicit_model_name(self, mock_get_client):
"""build_logs returns logs when served_model_name is provided."""
mock_client = mock.Mock()
mock_client.serving_endpoints.build_logs.return_value = mock.Mock(logs="Building image...\nDone.")
mock_get_client.return_value = mock_client

result = get_serving_endpoint_build_logs(name="test-ep", served_model_name="model-1")

assert result["name"] == "test-ep"
assert result["served_model_name"] == "model-1"
assert "Building image" in result["logs"]
mock_client.serving_endpoints.build_logs.assert_called_once_with(name="test-ep", served_model_name="model-1")

@mock.patch(_GET_CLIENT)
def test_build_logs_auto_resolve_model_name(self, mock_get_client):
"""build_logs auto-resolves served_model_name from endpoint config."""
mock_client = mock.Mock()
mock_client.serving_endpoints.get.return_value = _mock_endpoint(name="test-ep")
mock_client.serving_endpoints.build_logs.return_value = mock.Mock(logs="Build output")
mock_get_client.return_value = mock_client

result = get_serving_endpoint_build_logs(name="test-ep")

assert result["served_model_name"] == "model-1"
mock_client.serving_endpoints.get.assert_called_once_with(name="test-ep")

@mock.patch(_GET_CLIENT)
def test_build_logs_endpoint_not_found(self, mock_get_client):
"""build_logs raises when endpoint doesn't exist."""
mock_client = mock.Mock()
mock_client.serving_endpoints.get.side_effect = ResourceDoesNotExist("Not found")
mock_get_client.return_value = mock_client

with pytest.raises(Exception, match="not found"):
get_serving_endpoint_build_logs(name="missing-ep")

@mock.patch(_GET_CLIENT)
def test_build_logs_empty(self, mock_get_client):
"""build_logs returns empty string when no logs available."""
mock_client = mock.Mock()
mock_client.serving_endpoints.build_logs.return_value = mock.Mock(logs=None)
mock_get_client.return_value = mock_client

result = get_serving_endpoint_build_logs(name="test-ep", served_model_name="model-1")

assert result["logs"] == ""


class TestGetServingEndpointServerLogs:
"""Tests for get_serving_endpoint_server_logs."""

@mock.patch(_GET_CLIENT)
def test_server_logs_with_explicit_model_name(self, mock_get_client):
"""server_logs returns logs when served_model_name is provided."""
mock_client = mock.Mock()
mock_client.serving_endpoints.logs.return_value = mock.Mock(logs="Processing request...\n200 OK")
mock_get_client.return_value = mock_client

result = get_serving_endpoint_server_logs(name="test-ep", served_model_name="model-1")

assert result["name"] == "test-ep"
assert result["served_model_name"] == "model-1"
assert "Processing request" in result["logs"]
mock_client.serving_endpoints.logs.assert_called_once_with(name="test-ep", served_model_name="model-1")

@mock.patch(_GET_CLIENT)
def test_server_logs_auto_resolve_model_name(self, mock_get_client):
"""server_logs auto-resolves served_model_name from endpoint config."""
mock_client = mock.Mock()
mock_client.serving_endpoints.get.return_value = _mock_endpoint(name="test-ep")
mock_client.serving_endpoints.logs.return_value = mock.Mock(logs="Server output")
mock_get_client.return_value = mock_client

result = get_serving_endpoint_server_logs(name="test-ep")

assert result["served_model_name"] == "model-1"

@mock.patch(_GET_CLIENT)
def test_server_logs_endpoint_not_found(self, mock_get_client):
"""server_logs raises when endpoint doesn't exist."""
mock_client = mock.Mock()
mock_client.serving_endpoints.get.side_effect = ResourceDoesNotExist("Not found")
mock_get_client.return_value = mock_client

with pytest.raises(Exception, match="not found"):
get_serving_endpoint_server_logs(name="missing-ep")

@mock.patch(_GET_CLIENT)
def test_server_logs_served_model_not_found(self, mock_get_client):
"""server_logs raises when served model doesn't exist on endpoint."""
mock_client = mock.Mock()
mock_client.serving_endpoints.logs.side_effect = ResourceDoesNotExist("Served model not found")
mock_get_client.return_value = mock_client

with pytest.raises(Exception, match="not found"):
get_serving_endpoint_server_logs(name="test-ep", served_model_name="wrong-model")