Merge of arcade up and dev (#77)
Combination of these PRs: https://github.com/ArcadeAI/arcade-ai/pull/74 https://github.com/ArcadeAI/arcade-ai/pull/76
This commit is contained in:
parent
83cf070c82
commit
ef72e6c5aa
14 changed files with 418 additions and 141 deletions
8
Makefile
8
Makefile
|
|
@ -52,14 +52,6 @@ publish: ## publish a release to pypi.
|
||||||
.PHONY: build-and-publish
|
.PHONY: build-and-publish
|
||||||
build-and-publish: build publish ## Build and publish.
|
build-and-publish: build publish ## Build and publish.
|
||||||
|
|
||||||
.PHONY: docs-test
|
|
||||||
docs-test: ## Test if documentation can be built without warnings or errors
|
|
||||||
@cd arcade && poetry run mkdocs build -s
|
|
||||||
|
|
||||||
.PHONY: docs
|
|
||||||
docs: ## Build and serve the documentation
|
|
||||||
@cd arcade && poetry run mkdocs serve -a localhost:8777
|
|
||||||
|
|
||||||
.PHONY: docker
|
.PHONY: docker
|
||||||
docker: ## Build and run the Docker container
|
docker: ## Build and run the Docker container
|
||||||
@cd docker && make docker-build
|
@cd docker && make docker-build
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Callable, ClassVar
|
from typing import Any, Callable, ClassVar
|
||||||
|
|
||||||
|
from opentelemetry import trace
|
||||||
|
from opentelemetry.metrics import Meter
|
||||||
|
|
||||||
from arcade.actor.core.common import Actor, Router
|
from arcade.actor.core.common import Actor, Router
|
||||||
from arcade.actor.core.components import (
|
from arcade.actor.core.components import (
|
||||||
ActorComponent,
|
ActorComponent,
|
||||||
|
|
@ -36,7 +39,9 @@ class BaseActor(Actor):
|
||||||
HealthCheckComponent,
|
HealthCheckComponent,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, secret: str | None = None, disable_auth: bool = False) -> None:
|
def __init__(
|
||||||
|
self, secret: str | None = None, disable_auth: bool = False, otel_meter: Meter | None = None
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize the BaseActor with an empty ToolCatalog.
|
Initialize the BaseActor with an empty ToolCatalog.
|
||||||
If no secret is provided, the actor will use the ARCADE_ACTOR_SECRET environment variable.
|
If no secret is provided, the actor will use the ARCADE_ACTOR_SECRET environment variable.
|
||||||
|
|
@ -49,6 +54,11 @@ class BaseActor(Actor):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.secret = self._set_secret(secret, disable_auth)
|
self.secret = self._set_secret(secret, disable_auth)
|
||||||
|
self.environment = os.environ.get("ARCADE_ENVIRONMENT", "local")
|
||||||
|
if otel_meter:
|
||||||
|
self.tool_counter = otel_meter.create_counter(
|
||||||
|
"tool_call", "requests", "Total number of tools called"
|
||||||
|
)
|
||||||
|
|
||||||
def _set_secret(self, secret: str | None, disable_auth: bool) -> str:
|
def _set_secret(self, secret: str | None, disable_auth: bool) -> str:
|
||||||
if disable_auth:
|
if disable_auth:
|
||||||
|
|
@ -100,20 +110,52 @@ class BaseActor(Actor):
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
output = await ToolExecutor.run(
|
if self.tool_counter:
|
||||||
func=materialized_tool.tool,
|
self.tool_counter.add(
|
||||||
definition=materialized_tool.definition,
|
1,
|
||||||
input_model=materialized_tool.input_model,
|
{
|
||||||
output_model=materialized_tool.output_model,
|
"tool_name": tool_fqname.name,
|
||||||
context=tool_request.context,
|
"toolkit_version": str(tool_fqname.toolkit_version),
|
||||||
**tool_request.inputs or {},
|
"toolkit_name": tool_fqname.toolkit_name,
|
||||||
|
"environment": self.environment,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
invocation_id = tool_request.invocation_id or ""
|
||||||
|
logger.info(
|
||||||
|
f"{invocation_id} | Calling tool: {tool_fqname} version: {tool_request.tool.version}"
|
||||||
)
|
)
|
||||||
|
logger.debug(f"{invocation_id} | Tool inputs: {tool_request.inputs}")
|
||||||
|
|
||||||
|
tracer = trace.get_tracer(__name__)
|
||||||
|
with tracer.start_as_current_span("RunTool"):
|
||||||
|
output = await ToolExecutor.run(
|
||||||
|
func=materialized_tool.tool,
|
||||||
|
definition=materialized_tool.definition,
|
||||||
|
input_model=materialized_tool.input_model,
|
||||||
|
output_model=materialized_tool.output_model,
|
||||||
|
context=tool_request.context,
|
||||||
|
**tool_request.inputs or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
if output.error:
|
||||||
|
logger.warning(
|
||||||
|
f"{invocation_id} | Tool {tool_fqname} version {tool_request.tool.version} failed"
|
||||||
|
)
|
||||||
|
logger.warning(f"{invocation_id} | Tool error: {output.error.message}")
|
||||||
|
logger.debug(
|
||||||
|
f"{invocation_id} | Tool developer message: {output.error.developer_message}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"{invocation_id} | Tool {tool_fqname} version {tool_request.tool.version} success"
|
||||||
|
)
|
||||||
|
logger.debug(f"{invocation_id} | Tool output: {output}")
|
||||||
|
|
||||||
end_time = time.time() # End time in seconds
|
end_time = time.time() # End time in seconds
|
||||||
duration_ms = (end_time - start_time) * 1000 # Convert to milliseconds
|
duration_ms = (end_time - start_time) * 1000 # Convert to milliseconds
|
||||||
|
|
||||||
return ToolCallResponse(
|
return ToolCallResponse(
|
||||||
invocation_id=tool_request.invocation_id or "",
|
invocation_id=invocation_id,
|
||||||
duration=duration_ms,
|
duration=duration_ms,
|
||||||
finished_at=datetime.now().isoformat(),
|
finished_at=datetime.now().isoformat(),
|
||||||
success=not output.error,
|
success=not output.error,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from opentelemetry import trace
|
||||||
|
|
||||||
from arcade.actor.core.common import Actor, ActorComponent, RequestData, Router
|
from arcade.actor.core.common import Actor, ActorComponent, RequestData, Router
|
||||||
from arcade.core.schema import ToolCallRequest, ToolCallResponse, ToolDefinition
|
from arcade.core.schema import ToolCallRequest, ToolCallResponse, ToolDefinition
|
||||||
|
|
||||||
|
|
@ -18,7 +20,9 @@ class CatalogComponent(ActorComponent):
|
||||||
"""
|
"""
|
||||||
Handle the request to get the catalog.
|
Handle the request to get the catalog.
|
||||||
"""
|
"""
|
||||||
return self.actor.get_catalog()
|
tracer = trace.get_tracer(__name__)
|
||||||
|
with tracer.start_as_current_span("Catalog"):
|
||||||
|
return self.actor.get_catalog()
|
||||||
|
|
||||||
|
|
||||||
class CallToolComponent(ActorComponent):
|
class CallToolComponent(ActorComponent):
|
||||||
|
|
@ -35,9 +39,11 @@ class CallToolComponent(ActorComponent):
|
||||||
"""
|
"""
|
||||||
Handle the request to call (invoke) a tool.
|
Handle the request to call (invoke) a tool.
|
||||||
"""
|
"""
|
||||||
call_tool_request_data = request.body_json
|
tracer = trace.get_tracer(__name__)
|
||||||
call_tool_request = ToolCallRequest.model_validate(call_tool_request_data)
|
with tracer.start_as_current_span("CallTool"):
|
||||||
return await self.actor.call_tool(call_tool_request)
|
call_tool_request_data = request.body_json
|
||||||
|
call_tool_request = ToolCallRequest.model_validate(call_tool_request_data)
|
||||||
|
return await self.actor.call_tool(call_tool_request)
|
||||||
|
|
||||||
|
|
||||||
class HealthCheckComponent(ActorComponent):
|
class HealthCheckComponent(ActorComponent):
|
||||||
|
|
@ -54,4 +60,6 @@ class HealthCheckComponent(ActorComponent):
|
||||||
"""
|
"""
|
||||||
Handle the request for a health check.
|
Handle the request for a health check.
|
||||||
"""
|
"""
|
||||||
return self.actor.health_check()
|
tracer = trace.get_tracer(__name__)
|
||||||
|
with tracer.start_as_current_span("HealthCheck"):
|
||||||
|
return self.actor.health_check()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from typing import Any, Callable
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, Request
|
from fastapi import Depends, FastAPI, Request
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
from opentelemetry.metrics import Meter
|
||||||
|
|
||||||
from arcade.actor.core.base import (
|
from arcade.actor.core.base import (
|
||||||
BaseActor,
|
BaseActor,
|
||||||
|
|
@ -19,13 +20,18 @@ class FastAPIActor(BaseActor):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, app: FastAPI, secret: str | None = None, *, disable_auth: bool = False
|
self,
|
||||||
|
app: FastAPI,
|
||||||
|
secret: str | None = None,
|
||||||
|
*,
|
||||||
|
disable_auth: bool = False,
|
||||||
|
otel_meter: Meter | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize the FastAPIActor with a FastAPI app instance.
|
Initialize the FastAPIActor with a FastAPI app instance.
|
||||||
If no secret is provided, the actor will use the ARCADE_ACTOR_SECRET environment variable.
|
If no secret is provided, the actor will use the ARCADE_ACTOR_SECRET environment variable.
|
||||||
"""
|
"""
|
||||||
super().__init__(secret, disable_auth)
|
super().__init__(secret, disable_auth, otel_meter)
|
||||||
self.app = app
|
self.app = app
|
||||||
self.router = FastAPIRouter(app, self)
|
self.router = FastAPIRouter(app, self)
|
||||||
self.register_routes(self.router)
|
self.register_routes(self.router)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ def start_servers(
|
||||||
port: int,
|
port: int,
|
||||||
engine_config: str | None,
|
engine_config: str | None,
|
||||||
engine_env: dict[str, str] | None = None,
|
engine_env: dict[str, str] | None = None,
|
||||||
|
debug: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Start the actor and engine servers.
|
Start the actor and engine servers.
|
||||||
|
|
@ -39,11 +40,11 @@ def start_servers(
|
||||||
engine_config = _get_engine_config(engine_config)
|
engine_config = _get_engine_config(engine_config)
|
||||||
|
|
||||||
# Prepare command-line arguments for the actor server and engine
|
# Prepare command-line arguments for the actor server and engine
|
||||||
actor_cmd = _build_actor_command(host, port)
|
actor_cmd = _build_actor_command(host, port, debug)
|
||||||
engine_cmd = _build_engine_command(engine_config)
|
engine_cmd = _build_engine_command(engine_config)
|
||||||
|
|
||||||
# Start and manage the processes
|
# Start and manage the processes
|
||||||
_manage_processes(actor_cmd, engine_cmd, engine_env)
|
_manage_processes(actor_cmd, engine_cmd, engine_env, debug)
|
||||||
|
|
||||||
|
|
||||||
def _validate_host(host: str) -> str:
|
def _validate_host(host: str) -> str:
|
||||||
|
|
@ -109,6 +110,12 @@ def _get_engine_config(engine_config: str | None) -> str:
|
||||||
f"❌ Engine config file not found at {engine_config_path}", style="bold red"
|
f"❌ Engine config file not found at {engine_config_path}", style="bold red"
|
||||||
)
|
)
|
||||||
raise RuntimeError("Engine config file not found.")
|
raise RuntimeError("Engine config file not found.")
|
||||||
|
|
||||||
|
elif Path(os.path.expanduser("~/.arcade/engine.yaml")).is_file():
|
||||||
|
engine_config_path = Path(os.path.expanduser("~/.arcade/engine.yaml"))
|
||||||
|
console.print(
|
||||||
|
f"Using default engine config file at {engine_config_path}", style="bold green"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Look for engine.yaml in the current directory
|
# Look for engine.yaml in the current directory
|
||||||
engine_config_path = Path(os.getcwd()) / "engine.yaml"
|
engine_config_path = Path(os.getcwd()) / "engine.yaml"
|
||||||
|
|
@ -121,13 +128,14 @@ def _get_engine_config(engine_config: str | None) -> str:
|
||||||
return str(engine_config_path)
|
return str(engine_config_path)
|
||||||
|
|
||||||
|
|
||||||
def _build_actor_command(host: str, port: int) -> list[str]:
|
def _build_actor_command(host: str, port: int, debug: bool) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Builds the command to start the actor server.
|
Builds the command to start the actor server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
host: Host for the actor server.
|
host: Host for the actor server.
|
||||||
port: Port for the actor server.
|
port: Port for the actor server.
|
||||||
|
debug: Whether to run in debug mode.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The command as a list.
|
The command as a list.
|
||||||
|
|
@ -142,12 +150,14 @@ def _build_actor_command(host: str, port: int) -> list[str]:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
cmd = [
|
cmd = [
|
||||||
arcade_bin,
|
arcade_bin,
|
||||||
"dev",
|
"actorup",
|
||||||
"--host",
|
"--host",
|
||||||
host,
|
host,
|
||||||
"--port",
|
"--port",
|
||||||
str(port),
|
str(port),
|
||||||
]
|
]
|
||||||
|
if debug:
|
||||||
|
cmd.append("--debug")
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -165,13 +175,12 @@ def _build_engine_command(engine_config: str) -> list[str]:
|
||||||
if not engine_bin:
|
if not engine_bin:
|
||||||
console.print(
|
console.print(
|
||||||
"❌ Engine binary not found, refer to the installation guide at "
|
"❌ Engine binary not found, refer to the installation guide at "
|
||||||
"https://docs.arcade-ai.com/docs/home/deployment for how to install the engine",
|
"https://docs.arcade-ai.com/guides/installation for how to install the engine",
|
||||||
style="bold red",
|
style="bold red",
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
cmd = [
|
cmd = [
|
||||||
engine_bin,
|
engine_bin,
|
||||||
"dev",
|
|
||||||
"-c",
|
"-c",
|
||||||
engine_config,
|
engine_config,
|
||||||
]
|
]
|
||||||
|
|
@ -179,7 +188,10 @@ def _build_engine_command(engine_config: str) -> list[str]:
|
||||||
|
|
||||||
|
|
||||||
def _manage_processes(
|
def _manage_processes(
|
||||||
actor_cmd: list[str], engine_cmd: list[str], engine_env: dict[str, str] | None = None
|
actor_cmd: list[str],
|
||||||
|
engine_cmd: list[str],
|
||||||
|
engine_env: dict[str, str] | None = None,
|
||||||
|
debug: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Manages the lifecycle of the actor and engine processes.
|
Manages the lifecycle of the actor and engine processes.
|
||||||
|
|
@ -188,6 +200,7 @@ def _manage_processes(
|
||||||
actor_cmd: The command to start the actor server.
|
actor_cmd: The command to start the actor server.
|
||||||
engine_cmd: The command to start the engine.
|
engine_cmd: The command to start the engine.
|
||||||
engine_env: Environment variables to set for the engine.
|
engine_env: Environment variables to set for the engine.
|
||||||
|
debug: Whether to run in debug mode.
|
||||||
"""
|
"""
|
||||||
actor_process: subprocess.Popen | None = None
|
actor_process: subprocess.Popen | None = None
|
||||||
engine_process: subprocess.Popen | None = None
|
engine_process: subprocess.Popen | None = None
|
||||||
|
|
@ -208,14 +221,14 @@ def _manage_processes(
|
||||||
try:
|
try:
|
||||||
# Start the actor server
|
# Start the actor server
|
||||||
console.print("Starting actor server...", style="bold green")
|
console.print("Starting actor server...", style="bold green")
|
||||||
actor_process = _start_process("Actor", actor_cmd)
|
actor_process = _start_process("Actor", actor_cmd, debug=debug)
|
||||||
|
|
||||||
# Wait a bit to ensure actor is up
|
# Wait a bit to ensure actor is up
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# Start the engine
|
# Start the engine
|
||||||
console.print("Starting engine...", style="bold green")
|
console.print("Starting engine...", style="bold green")
|
||||||
engine_process = _start_process("Engine", engine_cmd, engine_env)
|
engine_process = _start_process("Engine", engine_cmd, env=engine_env, debug=debug)
|
||||||
|
|
||||||
# Monitor processes
|
# Monitor processes
|
||||||
_monitor_processes(actor_process, engine_process)
|
_monitor_processes(actor_process, engine_process)
|
||||||
|
|
@ -248,7 +261,7 @@ def _manage_processes(
|
||||||
|
|
||||||
|
|
||||||
def _start_process(
|
def _start_process(
|
||||||
name: str, cmd: list[str], env: dict[str, str] | None = None
|
name: str, cmd: list[str], env: dict[str, str] | None = None, debug: bool = False
|
||||||
) -> subprocess.Popen:
|
) -> subprocess.Popen:
|
||||||
"""
|
"""
|
||||||
Starts a subprocess and begins streaming its output.
|
Starts a subprocess and begins streaming its output.
|
||||||
|
|
@ -257,7 +270,7 @@ def _start_process(
|
||||||
name: Name of the process.
|
name: Name of the process.
|
||||||
cmd: Command to execute.
|
cmd: Command to execute.
|
||||||
env: Environment variables to set for the process.
|
env: Environment variables to set for the process.
|
||||||
|
debug: Whether to run in debug mode.
|
||||||
Returns:
|
Returns:
|
||||||
The subprocess.Popen object.
|
The subprocess.Popen object.
|
||||||
|
|
||||||
|
|
@ -268,8 +281,13 @@ def _start_process(
|
||||||
if env:
|
if env:
|
||||||
_env.update(env)
|
_env.update(env)
|
||||||
|
|
||||||
# TODO temporary fix for GIN_MODE
|
if debug:
|
||||||
_env["GIN_MODE"] = "release"
|
_env["GIN_MODE"] = "debug"
|
||||||
|
else:
|
||||||
|
_env["GIN_MODE"] = "release"
|
||||||
|
|
||||||
|
if name == "Actor":
|
||||||
|
_env["PYTHONUNBUFFERED"] = "1"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen( # noqa: S603, RUF100
|
process = subprocess.Popen( # noqa: S603, RUF100
|
||||||
|
|
@ -303,7 +321,12 @@ def _stream_output(process: subprocess.Popen, name: str) -> None:
|
||||||
return
|
return
|
||||||
with pipe:
|
with pipe:
|
||||||
for line in iter(pipe.readline, ""):
|
for line in iter(pipe.readline, ""):
|
||||||
console.print(f"[{style}]{name}>[/{style}] {line.rstrip()}")
|
line = line.rstrip()
|
||||||
|
if "WARNING" in line:
|
||||||
|
line = line.replace("WARNING", "[orange]WARNING[/orange]")
|
||||||
|
if "ERROR" in line:
|
||||||
|
line = line.replace("ERROR", "[red]ERROR[/red]")
|
||||||
|
console.print(f"[{style}]{name}>[/{style}] {line}")
|
||||||
|
|
||||||
threading.Thread(target=stream, args=(process.stdout, stdout_style), daemon=True).start()
|
threading.Thread(target=stream, args=(process.stdout, stdout_style), daemon=True).start()
|
||||||
threading.Thread(target=stream, args=(process.stderr, "red"), daemon=True).start()
|
threading.Thread(target=stream, args=(process.stderr, "red"), daemon=True).start()
|
||||||
|
|
|
||||||
|
|
@ -284,37 +284,6 @@ def chat(
|
||||||
raise typer.Exit()
|
raise typer.Exit()
|
||||||
|
|
||||||
|
|
||||||
@cli.command(help="Start a local Arcade Actor server", rich_help_panel="Launch")
|
|
||||||
def dev(
|
|
||||||
host: str = typer.Option(
|
|
||||||
"127.0.0.1", help="Host for the app, from settings by default.", show_default=True
|
|
||||||
),
|
|
||||||
port: int = typer.Option(
|
|
||||||
"8002", "-p", "--port", help="Port for the app, defaults to ", show_default=True
|
|
||||||
),
|
|
||||||
disable_auth: bool = typer.Option(
|
|
||||||
False,
|
|
||||||
"--no-auth",
|
|
||||||
help="Disable authentication for the actor. Not recommended for production.",
|
|
||||||
show_default=True,
|
|
||||||
),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Starts the actor with host, port, and reload options. Uses
|
|
||||||
Uvicorn as ASGI actor. Parameters allow runtime configuration.
|
|
||||||
"""
|
|
||||||
from arcade.cli.serve import serve_default_actor
|
|
||||||
|
|
||||||
try:
|
|
||||||
serve_default_actor(host, port, disable_auth)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
typer.Exit()
|
|
||||||
except Exception as e:
|
|
||||||
error_message = f"❌ Failed to start Arcade Actor: {escape(str(e))}"
|
|
||||||
console.print(error_message, style="bold red")
|
|
||||||
raise typer.Exit(code=1)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command(help="Show/edit the local Arcade configuration", rich_help_panel="User")
|
@cli.command(help="Show/edit the local Arcade configuration", rich_help_panel="User")
|
||||||
def config(
|
def config(
|
||||||
action: str = typer.Argument("show", help="The action to take (show/edit)"),
|
action: str = typer.Argument("show", help="The action to take (show/edit)"),
|
||||||
|
|
@ -500,8 +469,8 @@ def evals(
|
||||||
display_eval_results(results, show_details=show_details)
|
display_eval_results(results, show_details=show_details)
|
||||||
|
|
||||||
|
|
||||||
@cli.command(help="Start an Arcade Cluster instance", rich_help_panel="Launch")
|
@cli.command(help="Launch Arcade AI locally for tool dev", rich_help_panel="Launch")
|
||||||
def up(
|
def dev(
|
||||||
host: str = typer.Option("127.0.0.1", help="Host for the actor server.", show_default=True),
|
host: str = typer.Option("127.0.0.1", help="Host for the actor server.", show_default=True),
|
||||||
port: int = typer.Option(
|
port: int = typer.Option(
|
||||||
8002, "-p", "--port", help="Port for the actor server.", show_default=True
|
8002, "-p", "--port", help="Port for the actor server.", show_default=True
|
||||||
|
|
@ -509,14 +478,47 @@ def up(
|
||||||
engine_config: str = typer.Option(
|
engine_config: str = typer.Option(
|
||||||
None, "-c", "--config", help="Path to the engine configuration file."
|
None, "-c", "--config", help="Path to the engine configuration file."
|
||||||
),
|
),
|
||||||
|
debug: bool = typer.Option(False, "-d", "--debug", help="Show debug information"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Start both the actor and engine servers.
|
Start both the actor and engine servers.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# TODO: pass Engine env vars from here
|
# TODO: pass Engine env vars from here
|
||||||
start_servers(host, port, engine_config)
|
start_servers(host, port, engine_config, engine_env=None, debug=debug)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = f"❌ Failed to start servers: {escape(str(e))}"
|
error_message = f"❌ Failed to start servers: {escape(str(e))}"
|
||||||
console.print(error_message, style="bold red")
|
console.print(error_message, style="bold red")
|
||||||
typer.Exit(code=1)
|
typer.Exit(code=1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(help="Start a local Arcade Actor server", rich_help_panel="Launch", hidden=True)
|
||||||
|
def actorup(
|
||||||
|
host: str = typer.Option(
|
||||||
|
"127.0.0.1", help="Host for the app, from settings by default.", show_default=True
|
||||||
|
),
|
||||||
|
port: int = typer.Option(
|
||||||
|
"8002", "-p", "--port", help="Port for the app, defaults to ", show_default=True
|
||||||
|
),
|
||||||
|
disable_auth: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--no-auth",
|
||||||
|
help="Disable authentication for the actor. Not recommended for production.",
|
||||||
|
show_default=True,
|
||||||
|
),
|
||||||
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Starts the actor with host, port, and reload options. Uses
|
||||||
|
Uvicorn as ASGI actor. Parameters allow runtime configuration.
|
||||||
|
"""
|
||||||
|
from arcade.cli.serve import serve_default_actor
|
||||||
|
|
||||||
|
try:
|
||||||
|
serve_default_actor(host, port, disable_auth=disable_auth, debug=debug)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
typer.Exit()
|
||||||
|
except Exception as e:
|
||||||
|
error_message = f"❌ Failed to start Arcade Actor: {escape(str(e))}"
|
||||||
|
console.print(error_message, style="bold red")
|
||||||
|
typer.Exit(code=1)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ from typing import Any
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from arcade.core.telemetry import OTELHandler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import fastapi
|
import fastapi
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -60,9 +62,9 @@ def setup_logging(log_level: int = logging.INFO) -> None:
|
||||||
"sink": sys.stdout,
|
"sink": sys.stdout,
|
||||||
"serialize": False,
|
"serialize": False,
|
||||||
"level": log_level,
|
"level": log_level,
|
||||||
"format": "{time:MM-DD HH:mm:ss} | {level: <8} | {message}"
|
"format": "{level} [{time:HH:mm:ss.SSS}] {message}"
|
||||||
+ (" {name}:{function}:{line}" if log_level <= logging.DEBUG else "")
|
+ (" {name}:{function}:{line}" if log_level <= logging.DEBUG else "")
|
||||||
+ ("{exception}\n" if "{exception}" in "{message}" else ""),
|
+ ("\n{exception}" if "{exception}" in "{message}" else ""),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
@ -84,13 +86,15 @@ def serve_default_actor(
|
||||||
disable_auth: bool = False,
|
disable_auth: bool = False,
|
||||||
workers: int = 1,
|
workers: int = 1,
|
||||||
timeout_keep_alive: int = 5,
|
timeout_keep_alive: int = 5,
|
||||||
|
enable_otel: bool = False,
|
||||||
|
debug: bool = False,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Get an instance of a FastAPI server with the Arcade Actor.
|
Get an instance of a FastAPI server with the Arcade Actor.
|
||||||
"""
|
"""
|
||||||
# Setup unified logging
|
# Setup unified logging
|
||||||
setup_logging()
|
setup_logging(log_level=logging.DEBUG if debug else logging.INFO)
|
||||||
|
|
||||||
toolkits = Toolkit.find_all_arcade_toolkits()
|
toolkits = Toolkit.find_all_arcade_toolkits()
|
||||||
if not toolkits:
|
if not toolkits:
|
||||||
|
|
@ -114,7 +118,12 @@ def serve_default_actor(
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
lifespan=lifespan, # Use custom lifespan to catch errors, notably KeyboardInterrupt (Ctrl+C)
|
lifespan=lifespan, # Use custom lifespan to catch errors, notably KeyboardInterrupt (Ctrl+C)
|
||||||
)
|
)
|
||||||
actor = FastAPIActor(app, secret=actor_secret, disable_auth=disable_auth)
|
|
||||||
|
otel_handler = OTELHandler(app, enable=enable_otel)
|
||||||
|
|
||||||
|
actor = FastAPIActor(
|
||||||
|
app, secret=actor_secret, disable_auth=disable_auth, otel_meter=otel_handler.get_meter()
|
||||||
|
)
|
||||||
for toolkit in toolkits:
|
for toolkit in toolkits:
|
||||||
actor.register_toolkit(toolkit)
|
actor.register_toolkit(toolkit)
|
||||||
|
|
||||||
|
|
@ -143,4 +152,6 @@ def serve_default_actor(
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("Server stopped by user.")
|
logger.info("Server stopped by user.")
|
||||||
finally:
|
finally:
|
||||||
|
if enable_otel:
|
||||||
|
otel_handler.shutdown()
|
||||||
logger.debug("Server shutdown complete.")
|
logger.debug("Server shutdown complete.")
|
||||||
|
|
|
||||||
113
arcade/arcade/core/telemetry.py
Normal file
113
arcade/arcade/core/telemetry.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from opentelemetry import _logs, trace
|
||||||
|
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
||||||
|
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
||||||
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
||||||
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||||
|
from opentelemetry.metrics import Meter, get_meter_provider, set_meter_provider
|
||||||
|
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
||||||
|
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
||||||
|
from opentelemetry.sdk.metrics import MeterProvider
|
||||||
|
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
||||||
|
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
||||||
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||||
|
|
||||||
|
|
||||||
|
class ShutdownError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OTELHandler:
|
||||||
|
def __init__(self, app: FastAPI, enable: bool = True, log_level: int = logging.INFO):
|
||||||
|
self._tracer_provider: Optional[TracerProvider] = None
|
||||||
|
self._tracer_span_exporter: Optional[OTLPSpanExporter] = None
|
||||||
|
self._meter_provider: Optional[MeterProvider] = None
|
||||||
|
self._meter_reader: Optional[PeriodicExportingMetricReader] = None
|
||||||
|
self._otlp_metric_exporter: Optional[OTLPMetricExporter] = None
|
||||||
|
self._logger_provider: Optional[LoggerProvider] = None
|
||||||
|
self._log_processor: Optional[BatchLogRecordProcessor] = None
|
||||||
|
self.environment = os.environ.get("ARCADE_ENVIRONMENT", "local")
|
||||||
|
|
||||||
|
if enable:
|
||||||
|
logging.info(
|
||||||
|
"🔎 Initializing OpenTelemetry. Use environment variables to configure the connection"
|
||||||
|
)
|
||||||
|
self.resource = Resource(
|
||||||
|
attributes={SERVICE_NAME: "arcade-actor", "environment": self.environment}
|
||||||
|
)
|
||||||
|
|
||||||
|
self._init_tracer()
|
||||||
|
self._init_metrics()
|
||||||
|
self._init_logging(log_level)
|
||||||
|
|
||||||
|
FastAPIInstrumentor().instrument_app(app)
|
||||||
|
|
||||||
|
def _init_tracer(self) -> None:
|
||||||
|
self._tracer_provider = TracerProvider(resource=self.resource)
|
||||||
|
trace.set_tracer_provider(self._tracer_provider)
|
||||||
|
|
||||||
|
# Create an OTLP exporter
|
||||||
|
self._tracer_span_exporter = OTLPSpanExporter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._tracer_span_exporter.export([trace.get_tracer(__name__).start_span("ping")])
|
||||||
|
except Exception as e:
|
||||||
|
raise ConnectionError(
|
||||||
|
f"Could not connect to OpenTelemetry Tracer endpoint. Check OpenTelemetry configuration or disable: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a batch span processor and add the exporter
|
||||||
|
span_processor = BatchSpanProcessor(self._tracer_span_exporter)
|
||||||
|
self._tracer_provider.add_span_processor(span_processor)
|
||||||
|
|
||||||
|
def _init_metrics(self) -> None:
|
||||||
|
self._otlp_metric_exporter = OTLPMetricExporter()
|
||||||
|
|
||||||
|
self._meter_reader = PeriodicExportingMetricReader(self._otlp_metric_exporter)
|
||||||
|
|
||||||
|
self._meter_provider = MeterProvider(
|
||||||
|
metric_readers=[self._meter_reader], resource=self.resource
|
||||||
|
)
|
||||||
|
|
||||||
|
set_meter_provider(self._meter_provider)
|
||||||
|
|
||||||
|
def get_meter(self) -> Meter:
|
||||||
|
return get_meter_provider().get_meter(__name__)
|
||||||
|
|
||||||
|
def _init_logging(self, log_level: int) -> None:
|
||||||
|
otlp_log_exporter = OTLPLogExporter()
|
||||||
|
|
||||||
|
self._logger_provider = LoggerProvider(resource=self.resource)
|
||||||
|
_logs.set_logger_provider(self._logger_provider)
|
||||||
|
|
||||||
|
# Create a batch span processor and add the exporter
|
||||||
|
self._log_processor = BatchLogRecordProcessor(otlp_log_exporter)
|
||||||
|
self._logger_provider.add_log_record_processor(self._log_processor)
|
||||||
|
|
||||||
|
handler = LoggingHandler(level=log_level, logger_provider=self._logger_provider)
|
||||||
|
logging.getLogger().addHandler(handler)
|
||||||
|
|
||||||
|
def _shutdown_tracer(self) -> None:
|
||||||
|
if self._tracer_span_exporter is None:
|
||||||
|
raise ShutdownError("Tracer provider not initialized. Failed to shutdown")
|
||||||
|
self._tracer_span_exporter.shutdown()
|
||||||
|
|
||||||
|
def _shutdown_metrics(self) -> None:
|
||||||
|
if self._otlp_metric_exporter is None:
|
||||||
|
raise ShutdownError("Meter provider not initialized. Failed to shutdown")
|
||||||
|
self._otlp_metric_exporter.shutdown()
|
||||||
|
|
||||||
|
def _shutdown_logging(self) -> None:
|
||||||
|
if self._logger_provider is None:
|
||||||
|
raise ShutdownError("Log provider not initialized. Failed to shutdown")
|
||||||
|
self._logger_provider.shutdown()
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
self._shutdown_tracer()
|
||||||
|
self._shutdown_metrics()
|
||||||
|
self._shutdown_logging()
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
# arcade-ai
|
|
||||||
|
|
||||||
[](https://img.shields.io/github/v/release/spartee/arcade-ai)
|
|
||||||
[](https://github.com/spartee/arcade-ai/actions/workflows/main.yml?query=branch%3Amain)
|
|
||||||
[](https://img.shields.io/github/commit-activity/m/spartee/arcade-ai)
|
|
||||||
[](https://img.shields.io/github/license/spartee/arcade-ai)
|
|
||||||
|
|
||||||
Arcade AI python
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
site_name: arcade-ai
|
|
||||||
repo_url: https://github.com/spartee/arcade-ai
|
|
||||||
site_url: https://spartee.github.io/arcade-ai
|
|
||||||
site_description: Arcade AI python
|
|
||||||
site_author: Arcade AI
|
|
||||||
edit_uri: edit/main/docs/
|
|
||||||
repo_name: spartee/arcade-ai
|
|
||||||
copyright: Maintained by <a href="https://spartee.com">Florian</a>.
|
|
||||||
|
|
||||||
nav:
|
|
||||||
- Home: index.md
|
|
||||||
- Modules: modules.md
|
|
||||||
plugins:
|
|
||||||
- search
|
|
||||||
- mkdocstrings:
|
|
||||||
handlers:
|
|
||||||
python:
|
|
||||||
setup_commands:
|
|
||||||
- import sys
|
|
||||||
- sys.path.append('../')
|
|
||||||
theme:
|
|
||||||
name: material
|
|
||||||
feature:
|
|
||||||
tabs: true
|
|
||||||
palette:
|
|
||||||
- media: "(prefers-color-scheme: light)"
|
|
||||||
scheme: default
|
|
||||||
primary: white
|
|
||||||
accent: deep orange
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-7
|
|
||||||
name: Switch to dark mode
|
|
||||||
- media: "(prefers-color-scheme: dark)"
|
|
||||||
scheme: slate
|
|
||||||
primary: black
|
|
||||||
accent: deep orange
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-4
|
|
||||||
name: Switch to light mode
|
|
||||||
icon:
|
|
||||||
repo: fontawesome/brands/github
|
|
||||||
|
|
||||||
extra:
|
|
||||||
social:
|
|
||||||
- icon: fontawesome/brands/github
|
|
||||||
link: https://github.com/spartee/arcade-ai
|
|
||||||
- icon: fontawesome/brands/python
|
|
||||||
link: https://pypi.org/project/arcade-ai
|
|
||||||
|
|
||||||
markdown_extensions:
|
|
||||||
- toc:
|
|
||||||
permalink: true
|
|
||||||
- pymdownx.arithmatex:
|
|
||||||
generic: true
|
|
||||||
|
|
@ -22,6 +22,9 @@ tomlkit = "^0.12.4"
|
||||||
openai = "^1.36.0" # TODO: relax to an earlier version that still has what we need
|
openai = "^1.36.0" # TODO: relax to an earlier version that still has what we need
|
||||||
pyjwt = "^2.8.0"
|
pyjwt = "^2.8.0"
|
||||||
loguru = "^0.7.0"
|
loguru = "^0.7.0"
|
||||||
|
opentelemetry-instrumentation-fastapi = {version = "0.48b0", optional = true}
|
||||||
|
opentelemetry-exporter-otlp-proto-http = {version = "1.27.0", optional = true}
|
||||||
|
opentelemetry-exporter-otlp-proto-common = {version = "1.27.0", optional = true}
|
||||||
fastapi = {version = "^0.110.0", optional = true}
|
fastapi = {version = "^0.110.0", optional = true}
|
||||||
uvicorn = {version = "^0.30.0", optional = true}
|
uvicorn = {version = "^0.30.0", optional = true}
|
||||||
scipy = {version = "^1.14.0", optional = true}
|
scipy = {version = "^1.14.0", optional = true}
|
||||||
|
|
@ -29,7 +32,7 @@ numpy = {version = "^2.0.0", optional = true}
|
||||||
scikit-learn = {version = "^1.5.0", optional = true}
|
scikit-learn = {version = "^1.5.0", optional = true}
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
fastapi = ["fastapi", "uvicorn"]
|
fastapi = ["fastapi", "uvicorn", "opentelemetry-instrumentation-fastapi", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-exporter-otlp-proto-common"]
|
||||||
evals = ["scipy", "numpy", "scikit-learn"]
|
evals = ["scipy", "numpy", "scikit-learn"]
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|
|
||||||
138
arcade/tests/core/test_telemetry.py
Normal file
138
arcade/tests/core/test_telemetry.py
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from arcade.core.telemetry import OTELHandler, ShutdownError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app():
|
||||||
|
return FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def handler_disabled(app):
|
||||||
|
return OTELHandler(app, enable=False)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("arcade.core.telemetry.logging")
|
||||||
|
@patch("arcade.core.telemetry.FastAPIInstrumentor")
|
||||||
|
@patch("arcade.core.telemetry.OTLPLogExporter")
|
||||||
|
@patch("arcade.core.telemetry.OTLPMetricExporter")
|
||||||
|
@patch("arcade.core.telemetry.OTLPSpanExporter")
|
||||||
|
def test_init_with_enable_true(
|
||||||
|
mock_span_exporter,
|
||||||
|
mock_metric_exporter,
|
||||||
|
mock_log_exporter,
|
||||||
|
mock_instrumentor,
|
||||||
|
mock_logging,
|
||||||
|
app,
|
||||||
|
):
|
||||||
|
# Mock the methods that may cause network calls
|
||||||
|
mock_span_exporter.return_value.shutdown = MagicMock()
|
||||||
|
mock_metric_exporter.return_value.shutdown = MagicMock()
|
||||||
|
mock_log_exporter.return_value.shutdown = MagicMock()
|
||||||
|
|
||||||
|
# Initialize OTELHandler within the scope of the mocks
|
||||||
|
handler = OTELHandler(app, enable=True)
|
||||||
|
|
||||||
|
# Verify that the resource is set correctly
|
||||||
|
assert handler.resource.attributes["service.name"] == "arcade-actor"
|
||||||
|
assert "environment" in handler.resource.attributes
|
||||||
|
|
||||||
|
# Verify that initialization methods are called
|
||||||
|
assert handler._tracer_provider is not None
|
||||||
|
assert handler._tracer_span_exporter is not None
|
||||||
|
assert handler._meter_provider is not None
|
||||||
|
assert handler._meter_reader is not None
|
||||||
|
assert handler._logger_provider is not None
|
||||||
|
assert handler._log_processor is not None
|
||||||
|
|
||||||
|
# Verify that FastAPIInstrumentor is used
|
||||||
|
mock_instrumentor.return_value.instrument_app.assert_called_once_with(app)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("arcade.core.telemetry.logging")
|
||||||
|
@patch("arcade.core.telemetry.FastAPIInstrumentor")
|
||||||
|
def test_init_with_enable_false(mock_instrumentor, mock_logging, app):
|
||||||
|
handler = OTELHandler(app, enable=False)
|
||||||
|
|
||||||
|
# Verify that resources are not initialized
|
||||||
|
assert handler._tracer_provider is None
|
||||||
|
assert handler._tracer_span_exporter is None
|
||||||
|
assert handler._meter_provider is None
|
||||||
|
assert handler._meter_reader is None
|
||||||
|
assert handler._logger_provider is None
|
||||||
|
assert handler._log_processor is None
|
||||||
|
|
||||||
|
# Verify that FastAPIInstrumentor is not called
|
||||||
|
mock_instrumentor.return_value.instrument_app.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_tracer_export_exception(app):
|
||||||
|
# Simulate an exception during exporter initialization
|
||||||
|
|
||||||
|
with pytest.raises(ConnectionError) as exc_info:
|
||||||
|
OTELHandler(app, enable=True)
|
||||||
|
|
||||||
|
assert "Could not connect to OpenTelemetry Tracer endpoint" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("arcade.core.telemetry.OTLPLogExporter")
|
||||||
|
@patch("arcade.core.telemetry.OTLPMetricExporter")
|
||||||
|
@patch("arcade.core.telemetry.OTLPSpanExporter")
|
||||||
|
def test_shutdown(mock_span_exporter, mock_metric_exporter, mock_log_exporter, app):
|
||||||
|
# Mock the shutdown methods
|
||||||
|
mock_span_exporter.return_value.shutdown = MagicMock()
|
||||||
|
mock_metric_exporter.return_value.shutdown = MagicMock()
|
||||||
|
mock_log_exporter.return_value.shutdown = MagicMock()
|
||||||
|
|
||||||
|
handler = OTELHandler(app, enable=True)
|
||||||
|
|
||||||
|
# Call shutdown method
|
||||||
|
handler.shutdown()
|
||||||
|
|
||||||
|
# Verify that shutdown methods are called
|
||||||
|
mock_span_exporter.return_value.shutdown.assert_called_once()
|
||||||
|
mock_metric_exporter.return_value.shutdown.assert_called_once()
|
||||||
|
mock_log_exporter.return_value.shutdown.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_shutdown_tracer_not_initialized(handler_disabled):
|
||||||
|
with pytest.raises(ShutdownError) as exc_info:
|
||||||
|
handler_disabled._shutdown_tracer()
|
||||||
|
assert "Tracer provider not initialized" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shutdown_metrics_not_initialized(handler_disabled):
|
||||||
|
with pytest.raises(ShutdownError) as exc_info:
|
||||||
|
handler_disabled._shutdown_metrics()
|
||||||
|
assert "Meter provider not initialized" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shutdown_logging_not_initialized(handler_disabled):
|
||||||
|
with pytest.raises(ShutdownError) as exc_info:
|
||||||
|
handler_disabled._shutdown_logging()
|
||||||
|
assert "Log provider not initialized" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("arcade.core.telemetry.get_meter_provider")
|
||||||
|
@patch("arcade.core.telemetry.OTLPLogExporter")
|
||||||
|
@patch("arcade.core.telemetry.OTLPMetricExporter")
|
||||||
|
@patch("arcade.core.telemetry.OTLPSpanExporter")
|
||||||
|
def test_get_meter(
|
||||||
|
mock_span_exporter, mock_metric_exporter, mock_log_exporter, mock_get_meter_provider, app
|
||||||
|
):
|
||||||
|
# Mock the methods that may cause network calls
|
||||||
|
mock_span_exporter.return_value.shutdown = MagicMock()
|
||||||
|
mock_metric_exporter.return_value.shutdown = MagicMock()
|
||||||
|
mock_log_exporter.return_value.shutdown = MagicMock()
|
||||||
|
|
||||||
|
handler = OTELHandler(app, enable=True)
|
||||||
|
|
||||||
|
# Call get_meter method
|
||||||
|
handler.get_meter()
|
||||||
|
|
||||||
|
# Verify that get_meter_provider is called
|
||||||
|
mock_get_meter_provider.assert_called_once()
|
||||||
|
|
@ -8,6 +8,7 @@ ARG HOST=0.0.0.0
|
||||||
# Set environment variables using the build arguments
|
# Set environment variables using the build arguments
|
||||||
ENV PORT=${PORT}
|
ENV PORT=${PORT}
|
||||||
ENV HOST=${HOST}
|
ENV HOST=${HOST}
|
||||||
|
ENV OTEL_ENABLE=false
|
||||||
ENV ARCADE_WORK_DIR=/app
|
ENV ARCADE_WORK_DIR=/app
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
|
|
@ -53,5 +54,5 @@ RUN set -e; \
|
||||||
# Expose the port
|
# Expose the port
|
||||||
EXPOSE $PORT
|
EXPOSE $PORT
|
||||||
|
|
||||||
# Run the arcade dev command
|
# Run the arcade actorup (hidden cli command)
|
||||||
CMD arcade dev --host $HOST --port $PORT
|
CMD arcade actorup --host $HOST --port $PORT
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue