chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,405 @@
import asyncio
import concurrent.futures
import io
import logging
import os
from pathlib import Path
from typing import AsyncIterator, Optional
import grpc
import ray.dashboard.modules.log.log_consts as log_consts
import ray.dashboard.modules.log.log_utils as log_utils
import ray.dashboard.optional_utils as dashboard_optional_utils
import ray.dashboard.utils as dashboard_utils
from ray._private.ray_constants import env_integer
from ray.core.generated import reporter_pb2, reporter_pb2_grpc
logger = logging.getLogger(__name__)
routes = dashboard_optional_utils.DashboardAgentRouteTable
# 64 KB
BLOCK_SIZE = 1 << 16
# Keep-alive interval for reading the file
DEFAULT_KEEP_ALIVE_INTERVAL_SEC = 1
RAY_DASHBOARD_LOG_TASK_LOG_SEARCH_MAX_WORKER_COUNT = env_integer(
"RAY_DASHBOARD_LOG_TASK_LOG_SEARCH_MAX_WORKER_COUNT", default=2
)
def find_offset_of_content_in_file(
file: io.BufferedIOBase, content: bytes, start_offset: int = 0
) -> int:
"""Find the offset of the first occurrence of content in a file.
Args:
file: File object
content: Content to find
start_offset: Start offset to read from, inclusive.
Returns:
Offset of the first occurrence of content in a file.
"""
logger.debug(f"Finding offset of content {content} in file")
file.seek(start_offset, io.SEEK_SET) # move file pointer to start of file
offset = start_offset
while True:
# Read in block
block_data = file.read(BLOCK_SIZE)
if block_data == b"":
# Stop reading
return -1
# Find the offset of the first occurrence of content in the block
block_offset = block_data.find(content)
if block_offset != -1:
# Found the offset in the block
return offset + block_offset
# Continue reading
offset += len(block_data)
def find_end_offset_file(file: io.BufferedIOBase) -> int:
"""
Find the offset of the end of a file without changing the file pointer.
Args:
file: File object
Returns:
Offset of the end of a file.
"""
old_pos = file.tell() # store old position
file.seek(0, io.SEEK_END) # move file pointer to end of file
end = file.tell() # return end of file offset
file.seek(old_pos, io.SEEK_SET)
return end
def find_end_offset_next_n_lines_from_offset(
file: io.BufferedIOBase, start_offset: int, n: int
) -> int:
"""
Find the offsets of next n lines from a start offset.
Args:
file: File object
start_offset: Start offset to read from, inclusive.
n: Number of lines to find.
Returns:
Offset of the end of the next n line (exclusive)
"""
file.seek(start_offset) # move file pointer to start offset
end_offset = None
for _ in range(n): # loop until we find n lines or reach end of file
line = file.readline() # read a line and consume new line character
if not line: # end of file
break
end_offset = file.tell() # end offset.
logger.debug(f"Found next {n} lines from {start_offset} offset")
return (
end_offset if end_offset is not None else file.seek(0, io.SEEK_END)
) # return last line offset or end of file offset if no lines found
def find_start_offset_last_n_lines_from_offset(
file: io.BufferedIOBase, offset: int, n: int, block_size: int = BLOCK_SIZE
) -> int:
"""
Find the offset of the beginning of the line of the last X lines from an offset.
Args:
file: File object
offset: Start offset from which to find last X lines, -1 means end of file.
The offset is exclusive, i.e. data at the offset is not included
in the result.
n: Number of lines to find
block_size: Block size to read from file
Returns:
Offset of the beginning of the line of the last X lines from a start offset.
"""
logger.debug(f"Finding last {n} lines from {offset} offset")
if offset == -1:
offset = file.seek(0, io.SEEK_END) # move file pointer to end of file
else:
file.seek(offset, io.SEEK_SET) # move file pointer to start offset
if n == 0:
return offset
nbytes_from_end = (
0 # Number of bytes that should be tailed from the end of the file
)
# Non new line terminating offset, adjust the line count and treat the non-newline
# terminated line as the last line. e.g. line 1\nline 2
file.seek(max(0, offset - 1), os.SEEK_SET)
if file.read(1) != b"\n":
n -= 1
# Remaining number of lines to tail
lines_more = n
read_offset = max(0, offset - block_size)
# So that we know how much to read on the last block (the block 0)
prev_offset = offset
while lines_more >= 0 and read_offset >= 0:
# Seek to the current block start
file.seek(read_offset, 0)
# Read the current block (or less than block) data
block_data = file.read(min(block_size, prev_offset - read_offset))
num_lines = block_data.count(b"\n")
if num_lines > lines_more:
# This is the last block to read.
# Need to find the offset of exact number of lines to tail
# in the block.
# Use `split` here to split away the extra lines, i.e.
# first `num_lines - lines_more` lines.
lines = block_data.split(b"\n", num_lines - lines_more)
# Added the len of those lines that at the end of the block.
nbytes_from_end += len(lines[-1])
break
# Need to read more blocks.
lines_more -= num_lines
nbytes_from_end += len(block_data)
if read_offset == 0:
# We have read all blocks (since the start)
break
# Continuing with the previous block
prev_offset = read_offset
read_offset = max(0, read_offset - block_size)
offset_read_start = offset - nbytes_from_end
assert (
offset_read_start >= 0
), f"Read start offset({offset_read_start}) should be non-negative"
return offset_read_start
async def _stream_log_in_chunk(
context: grpc.aio.ServicerContext,
file: io.BufferedIOBase,
start_offset: int,
end_offset: int = -1,
keep_alive_interval_sec: int = -1,
block_size: int = BLOCK_SIZE,
) -> AsyncIterator[reporter_pb2.StreamLogReply]:
"""Streaming log in chunk from start to end offset.
Stream binary file content in chunks from start offset to an end
offset if provided, else to the end of the file.
Args:
context: gRPC server side context
file: Binary file to stream
start_offset: File offset where streaming starts
end_offset: If -1, implying streaming til the EOF.
keep_alive_interval_sec: Duration for which streaming will be
retried when reaching the file end, -1 means no retry.
block_size: Number of bytes per chunk, exposed for testing
Yields:
reporter_pb2.StreamLogReply: Successive chunks of the file contents,
one per block.
"""
assert "b" in file.mode, "Only binary file is supported."
assert not (
keep_alive_interval_sec >= 0 and end_offset != -1
), "Keep-alive is not allowed when specifying an end offset"
file.seek(start_offset, 0)
cur_offset = start_offset
# Until gRPC is done
while not context.done():
# Read in block
if end_offset != -1:
to_read = min(end_offset - cur_offset, block_size)
else:
to_read = block_size
bytes = file.read(to_read)
if bytes == b"":
# Stop reading
if keep_alive_interval_sec >= 0:
await asyncio.sleep(keep_alive_interval_sec)
# Try reading again
continue
# Have read the entire file, done
break
logger.debug(f"Sending {len(bytes)} bytes at {cur_offset}")
yield reporter_pb2.StreamLogReply(data=bytes)
# Have read the requested section [start_offset, end_offset), done
cur_offset += len(bytes)
if end_offset != -1 and cur_offset >= end_offset:
break
class LogAgent(dashboard_utils.DashboardAgentModule):
def __init__(self, dashboard_agent):
super().__init__(dashboard_agent)
log_utils.register_mimetypes()
routes.static("/logs", self._dashboard_agent.log_dir, show_index=True)
async def run(self, server):
pass
@staticmethod
def is_minimal_module():
return False
_task_log_search_worker_pool = concurrent.futures.ThreadPoolExecutor(
max_workers=RAY_DASHBOARD_LOG_TASK_LOG_SEARCH_MAX_WORKER_COUNT
)
class LogAgentV1Grpc(dashboard_utils.DashboardAgentModule):
def __init__(self, dashboard_agent):
super().__init__(dashboard_agent)
async def run(self, server):
if server:
reporter_pb2_grpc.add_LogServiceServicer_to_server(self, server)
@property
def node_id(self) -> Optional[str]:
return self._dashboard_agent.get_node_id()
@staticmethod
def is_minimal_module():
# Dashboard is only available with non-minimal install now.
return False
async def ListLogs(self, request, context):
"""
Lists all files in the active Ray logs directory.
Part of `LogService` gRPC.
NOTE: These RPCs are used by state_head.py, not log_head.py
"""
path = Path(self._dashboard_agent.log_dir)
if not path.exists():
raise FileNotFoundError(
f"Could not find log dir at path: {self._dashboard_agent.log_dir}"
"It is unexpected. Please report an issue to Ray Github."
)
log_files = []
for p in path.glob(request.glob_filter):
log_files.append(str(p.relative_to(path)) + ("/" if p.is_dir() else ""))
return reporter_pb2.ListLogsReply(log_files=log_files)
@classmethod
def _resolve_filename(cls, root_log_dir: Path, filename: str) -> Path:
"""
Resolves the file path relative to the root log directory.
Args:
root_log_dir: Root log directory.
filename: File path relative to the root log directory.
Raises:
FileNotFoundError: If the file path is invalid.
Returns:
The absolute file path resolved from the root log directory.
"""
if not Path(filename).is_absolute():
filepath = root_log_dir / filename
else:
filepath = Path(filename)
# We want to allow relative paths that include symlinks pointing outside of the
# `root_log_dir`, so use `os.path.abspath` instead of `Path.resolve()` because
# `os.path.abspath` does not resolve symlinks.
filepath = Path(os.path.abspath(filepath))
if not filepath.is_file():
raise FileNotFoundError(f"A file is not found at: {filepath}")
try:
filepath.relative_to(root_log_dir)
except ValueError as e:
raise FileNotFoundError(f"{filepath} not in {root_log_dir}: {e}")
# Fully resolve the path before returning (including following symlinks).
return filepath.resolve()
async def StreamLog(self, request, context):
"""
Streams the log in real time starting from `request.lines` number of lines from
the end of the file if `request.keep_alive == True`. Else, it terminates the
stream once there are no more bytes to read from the log file.
Part of `LogService` gRPC.
NOTE: These RPCs are used by state_head.py, not log_head.py
"""
# NOTE: If the client side connection is closed, this handler will
# be automatically terminated.
lines = request.lines if request.lines else 1000
try:
filepath = self._resolve_filename(
Path(self._dashboard_agent.log_dir), request.log_file_name
)
except FileNotFoundError as e:
await context.send_initial_metadata([[log_consts.LOG_GRPC_ERROR, str(e)]])
else:
with open(filepath, "rb") as f:
await context.send_initial_metadata([])
# Default stream entire file
start_offset = (
request.start_offset if request.HasField("start_offset") else 0
)
end_offset = (
request.end_offset
if request.HasField("end_offset")
else find_end_offset_file(f)
)
if lines != -1:
# If specified tail line number, cap the start offset
# with lines from the current end offset
start_offset = max(
find_start_offset_last_n_lines_from_offset(
f, offset=end_offset, n=lines
),
start_offset,
)
# If keep alive: following the log every 'interval'
keep_alive_interval_sec = -1
if request.keep_alive:
keep_alive_interval_sec = (
request.interval
if request.interval
else DEFAULT_KEEP_ALIVE_INTERVAL_SEC
)
# When following (keep_alive), it will read beyond the end
end_offset = -1
logger.info(
f"Tailing logs from {start_offset} to {end_offset} for "
f"lines={lines}, with keep_alive={keep_alive_interval_sec}"
)
# Read and send the file data in chunk
async for chunk_res in _stream_log_in_chunk(
context=context,
file=f,
start_offset=start_offset,
end_offset=end_offset,
keep_alive_interval_sec=keep_alive_interval_sec,
):
yield chunk_res
@@ -0,0 +1,8 @@
MIME_TYPES = {
"text/plain": [".err", ".out", ".log"],
}
LOG_GRPC_ERROR = "log_grpc_status"
# 10 seconds
GRPC_TIMEOUT = 10
@@ -0,0 +1,478 @@
import logging
import re
from collections import defaultdict
from typing import AsyncIterable, Awaitable, Callable, Dict, List, Optional, Tuple
from ray import ActorID, NodeID, WorkerID
from ray._common.pydantic_compat import BaseModel
from ray.core.generated.gcs_pb2 import ActorTableData
from ray.dashboard.modules.job.common import JOB_LOGS_PATH_TEMPLATE
from ray.util.state.common import (
DEFAULT_RPC_TIMEOUT,
GetLogOptions,
protobuf_to_task_state_dict,
)
from ray.util.state.state_manager import StateDataSourceClient
if BaseModel is None:
raise ModuleNotFoundError("Please install pydantic via `pip install pydantic`.")
logger = logging.getLogger(__name__)
WORKER_LOG_PATTERN = re.compile(r".*worker-([0-9a-f]+)-([0-9a-f]+)-(\d+).(out|err)")
class ResolvedStreamFileInfo(BaseModel):
# The node id where the log file is located.
node_id: str
# The log file path name. Could be a relative path relative to ray's logging folder,
# or an absolute path.
filename: str
# Start offset in the log file to stream from. None to indicate beginning of
# the file, or determined by last tail lines.
start_offset: Optional[int] = None
# End offset in the log file to stream from. None to indicate the end of the file.
end_offset: Optional[int] = None
class LogsManager:
def __init__(self, data_source_client: StateDataSourceClient):
self.client = data_source_client
@property
def data_source_client(self) -> StateDataSourceClient:
return self.client
async def ip_to_node_id(self, node_ip: Optional[str]) -> Optional[str]:
"""Resolve the node id in hex from a given node ip.
Args:
node_ip: The node ip.
Returns:
node_id if there's a node id that matches the given node ip and is alive.
None otherwise.
"""
return await self.client.ip_to_node_id(node_ip)
async def list_logs(
self, node_id: str, timeout: int, glob_filter: str = "*"
) -> Dict[str, List[str]]:
"""Return a list of log files on a given node id filtered by the glob.
Args:
node_id: The node id where log files present.
timeout: The timeout of the API.
glob_filter: The glob filter to filter out log files.
Returns:
Dictionary of {component_name -> list of log files}
Raises:
ValueError: If a source is unresponsive.
"""
reply = await self.client.list_logs(node_id, glob_filter, timeout=timeout)
return self._categorize_log_files(reply.log_files)
async def stream_logs(
self,
options: GetLogOptions,
get_actor_fn: Callable[[ActorID], Awaitable[Optional[ActorTableData]]],
) -> AsyncIterable[bytes]:
"""Generate a stream of logs in bytes.
Args:
options: The option for streaming logs.
get_actor_fn: Callable used to resolve actor metadata when the
request targets an actor's logs.
Yields:
bytes: Successive chunks of log content streamed from the agent.
"""
node_id = options.node_id
if node_id is None:
node_id = await self.ip_to_node_id(options.node_ip)
res = await self.resolve_filename(
node_id=node_id,
log_filename=options.filename,
actor_id=options.actor_id,
task_id=options.task_id,
attempt_number=options.attempt_number,
pid=options.pid,
get_actor_fn=get_actor_fn,
timeout=options.timeout,
suffix=options.suffix,
submission_id=options.submission_id,
)
keep_alive = options.media_type == "stream"
stream = await self.client.stream_log(
node_id=res.node_id,
log_file_name=res.filename,
keep_alive=keep_alive,
lines=options.lines,
interval=options.interval,
# If we keepalive logs connection, we shouldn't have timeout
# otherwise the stream will be terminated forcefully
# after the deadline is expired.
timeout=options.timeout if not keep_alive else None,
start_offset=res.start_offset,
end_offset=res.end_offset,
)
async for streamed_log in stream:
yield streamed_log.data
async def _resolve_job_filename(self, sub_job_id: str) -> Tuple[str, str]:
"""Return the log file name and node id for a given job submission id.
Args:
sub_job_id: The job submission id.
Returns:
The log file name and node id.
"""
job_infos = await self.client.get_job_info(timeout=DEFAULT_RPC_TIMEOUT)
target_job = None
for job_info in job_infos:
if job_info.submission_id == sub_job_id:
target_job = job_info
break
if target_job is None:
logger.info(f"Submission job ID {sub_job_id} not found.")
return None, None
node_id = job_info.driver_node_id
if node_id is None:
raise ValueError(
f"Job {sub_job_id} has no driver node id info. "
"This is likely a bug. Please file an issue."
)
log_filename = JOB_LOGS_PATH_TEMPLATE.format(submission_id=sub_job_id)
return node_id, log_filename
async def _resolve_worker_file(
self,
node_id_hex: str,
worker_id_hex: Optional[str],
pid: Optional[int],
suffix: str,
timeout: int,
) -> Optional[str]:
"""Resolve worker log file."""
if worker_id_hex is not None and pid is not None:
raise ValueError(
f"Only one of worker id({worker_id_hex}) or pid({pid}) should be"
"provided."
)
if worker_id_hex is not None:
log_files = await self.list_logs(
node_id_hex, timeout, glob_filter=f"*{worker_id_hex}*{suffix}"
)
else:
log_files = await self.list_logs(
node_id_hex, timeout, glob_filter=f"*{pid}*{suffix}"
)
# Find matching worker logs.
for filename in [*log_files["worker_out"], *log_files["worker_err"]]:
# Worker logs look like worker-[worker_id]-[job_id]-[pid].out
if worker_id_hex is not None:
worker_id_from_filename = WORKER_LOG_PATTERN.match(filename).group(1)
if worker_id_from_filename == worker_id_hex:
return filename
else:
worker_pid_from_filename = int(
WORKER_LOG_PATTERN.match(filename).group(3)
)
if worker_pid_from_filename == pid:
return filename
return None
async def _resolve_actor_filename(
self,
actor_id: ActorID,
get_actor_fn: Callable[[ActorID], Awaitable[Optional[ActorTableData]]],
suffix: str,
timeout: int,
):
"""Resolve actor log file.
Args:
actor_id: The actor id.
get_actor_fn: The function to get actor information.
suffix: The suffix of the log file.
timeout: Timeout in seconds.
Returns:
The log file name and node id.
Raises:
ValueError: If actor data is not found or get_actor_fn is not provided.
"""
if get_actor_fn is None:
raise ValueError("get_actor_fn needs to be specified for actor_id")
actor_data = await get_actor_fn(actor_id)
if actor_data is None:
raise ValueError(f"Actor ID {actor_id} not found.")
# TODO(sang): Only the latest worker id can be obtained from
# actor information now. That means, if actors are restarted,
# there's no way for us to get the past worker ids.
worker_id_binary = actor_data.address.worker_id
if not worker_id_binary:
raise ValueError(
f"Worker ID for Actor ID {actor_id} not found. "
"Actor is not scheduled yet."
)
worker_id = WorkerID(worker_id_binary)
node_id_binary = actor_data.address.node_id
if not node_id_binary:
raise ValueError(
f"Node ID for Actor ID {actor_id} not found. "
"Actor is not scheduled yet."
)
node_id = NodeID(node_id_binary)
log_filename = await self._resolve_worker_file(
node_id_hex=node_id.hex(),
worker_id_hex=worker_id.hex(),
pid=None,
suffix=suffix,
timeout=timeout,
)
return node_id.hex(), log_filename
async def _resolve_task_filename(
self, task_id: str, attempt_number: int, suffix: str, timeout: int
):
"""Resolve log file for a task.
Args:
task_id: The task id.
attempt_number: The attempt number.
suffix: The suffix of the log file, e.g. out or err.
timeout: Timeout in seconds.
Returns:
The log file name, node id, the start and end offsets of the
corresponding task log in the file.
Raises:
FileNotFoundError: If the log file is not found.
ValueError: If the suffix is not out or err.
"""
log_filename = None
node_id = None
start_offset = None
end_offset = None
if suffix not in ["out", "err"]:
raise ValueError(f"Suffix {suffix} is not supported.")
reply = await self.client.get_all_task_info(
filters=[("task_id", "=", task_id)], timeout=timeout
)
# Check if the task is found.
if len(reply.events_by_task) == 0:
raise FileNotFoundError(
f"Could not find log file for task: {task_id}"
f" (attempt {attempt_number}) with suffix: {suffix}"
)
task_event = None
for t in reply.events_by_task:
if t.attempt_number == attempt_number:
task_event = t
break
if task_event is None:
raise FileNotFoundError(
"Could not find log file for task attempt:"
f"{task_id}({attempt_number})"
)
# Get the worker id and node id.
task = protobuf_to_task_state_dict(task_event)
worker_id = task.get("worker_id", None)
node_id = task.get("node_id", None)
log_info = task.get("task_log_info", None)
actor_id = task.get("actor_id", None)
if node_id is None:
raise FileNotFoundError(
"Could not find log file for task attempt."
f"{task_id}({attempt_number}) due to missing node info."
)
if log_info is None and actor_id is not None:
# This is a concurrent actor task. The logs will be interleaved.
# So we return the log file of the actor instead.
raise FileNotFoundError(
f"For actor task, please query actor log for "
f"actor({actor_id}): e.g. ray logs actor --id {actor_id} . Or "
"set RAY_ENABLE_RECORD_ACTOR_TASK_LOGGING=1 in actor's runtime env "
"or when starting the cluster. Recording actor task's log could be "
"expensive, so Ray turns it off by default."
)
elif log_info is None:
raise FileNotFoundError(
"Could not find log file for task attempt:"
f"{task_id}({attempt_number})."
f"Worker id = {worker_id}, node id = {node_id},"
f"log_info = {log_info}"
)
filename_key = "stdout_file" if suffix == "out" else "stderr_file"
log_filename = log_info.get(filename_key, None)
if log_filename is None:
raise FileNotFoundError(
f"Missing log filename info in {log_info} for task {task_id},"
f"attempt {attempt_number}"
)
start_offset = log_info.get(f"std{suffix}_start", None)
end_offset = log_info.get(f"std{suffix}_end", None)
return node_id, log_filename, start_offset, end_offset
async def resolve_filename(
self,
*,
node_id: Optional[str] = None,
log_filename: Optional[str] = None,
actor_id: Optional[str] = None,
task_id: Optional[str] = None,
attempt_number: Optional[int] = None,
pid: Optional[str] = None,
get_actor_fn: Optional[
Callable[[ActorID], Awaitable[Optional[ActorTableData]]]
] = None,
timeout: int = DEFAULT_RPC_TIMEOUT,
suffix: str = "out",
submission_id: Optional[str] = None,
) -> ResolvedStreamFileInfo:
"""Return the file name given all options.
Args:
node_id: The node's id from which logs are resolved.
log_filename: Filename of the log file.
actor_id: Id of the actor that generates the log file.
task_id: Id of the task that generates the log file.
attempt_number: The attempt number of the task. Used with
``task_id`` to disambiguate retries.
pid: Id of the worker process that generates the log file.
get_actor_fn: Callback to get the actor's data by id.
timeout: Timeout for the gRPC to listing logs on the node
specified by `node_id`.
suffix: Log suffix if no `log_filename` is provided, when
resolving by other ids'. Default to "out".
submission_id: The submission id for a submission job.
Returns:
A ``ResolvedStreamFileInfo`` describing the resolved node id,
filename, and (optional) byte offsets to stream.
"""
start_offset = None
end_offset = None
if suffix not in ["out", "err"]:
raise ValueError(f"Suffix {suffix} is not supported. ")
# TODO(rickyx): We should make sure we do some sort of checking on the log
# filename
if actor_id:
node_id, log_filename = await self._resolve_actor_filename(
ActorID.from_hex(actor_id), get_actor_fn, suffix, timeout
)
elif task_id:
(
node_id,
log_filename,
start_offset,
end_offset,
) = await self._resolve_task_filename(
task_id, attempt_number, suffix, timeout
)
elif submission_id:
node_id, log_filename = await self._resolve_job_filename(submission_id)
elif pid:
if node_id is None:
raise ValueError(
"Node id needs to be specified for resolving"
f" filenames of pid {pid}"
)
log_filename = await self._resolve_worker_file(
node_id_hex=node_id,
worker_id_hex=None,
pid=pid,
suffix=suffix,
timeout=timeout,
)
if log_filename is None:
raise FileNotFoundError(
"Could not find a log file. Please make sure the given "
"option exists in the cluster.\n"
f"\tnode_id: {node_id}\n"
f"\tfilename: {log_filename}\n"
f"\tactor_id: {actor_id}\n"
f"\ttask_id: {task_id}\n"
f"\tpid: {pid}\n"
f"\tsuffix: {suffix}\n"
f"\tsubmission_id: {submission_id}\n"
f"\tattempt_number: {attempt_number}\n"
)
res = ResolvedStreamFileInfo(
node_id=node_id,
filename=log_filename,
start_offset=start_offset,
end_offset=end_offset,
)
logger.info(f"Resolved log file: {res}")
return res
def _categorize_log_files(self, log_files: List[str]) -> Dict[str, List[str]]:
"""Categorize the given log files after filterieng them out using a given glob.
Args:
log_files: Filenames returned from a ``list_logs`` query, already
filtered by the caller's glob.
Returns:
Dictionary of {component_name -> list of log files}
"""
result = defaultdict(list)
for log_file in log_files:
if "worker" in log_file and (log_file.endswith(".out")):
result["worker_out"].append(log_file)
elif "worker" in log_file and (log_file.endswith(".err")):
result["worker_err"].append(log_file)
elif "core-worker" in log_file and log_file.endswith(".log"):
result["core_worker"].append(log_file)
elif "core-driver" in log_file and log_file.endswith(".log"):
result["driver"].append(log_file)
elif "raylet." in log_file:
result["raylet"].append(log_file)
elif "gcs_server." in log_file:
result["gcs_server"].append(log_file)
elif "log_monitor" in log_file:
result["internal"].append(log_file)
elif "monitor" in log_file:
result["autoscaler"].append(log_file)
elif "agent." in log_file:
result["agent"].append(log_file)
elif "dashboard." in log_file:
result["dashboard"].append(log_file)
else:
result["internal"].append(log_file)
return result
@@ -0,0 +1,9 @@
import mimetypes
import ray.dashboard.modules.log.log_consts as log_consts
def register_mimetypes():
for _type, extensions in log_consts.MIME_TYPES.items():
for ext in extensions:
mimetypes.add_type(_type, ext)