Skip to content

Commit

Permalink
Merge pull request #32 from simonsobs/JBorrow/track-errors
Browse files Browse the repository at this point in the history
Add core error logging framework
  • Loading branch information
JBorrow authored Jan 30, 2024
2 parents 3c6400c + 4200bb4 commit 083d6be
Show file tree
Hide file tree
Showing 27 changed files with 1,405 additions and 195 deletions.
63 changes: 20 additions & 43 deletions alembic/versions/71df5b41ae41_initial_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,16 @@
branch_labels = None
depends_on = None

from alembic import op
from sqlalchemy import (
Column,
DateTime,
BigInteger,
String,
Integer,
PrimaryKeyConstraint,
ForeignKey,
Enum,
PickleType,
Boolean,
)

import enum

from sqlalchemy import (BigInteger, Boolean, Column, DateTime, Enum,
ForeignKey, Integer, PickleType, PrimaryKeyConstraint,
String)

class DeletionPolicy(enum.Enum):
"""
Enumeration for whether or not a file can be deleted from a store.
Always defaults to 'DISALLOWED' when parsing.
"""

DISALLOWED = 0
ALLOWED = 1


class TransferStatus(enum.Enum):
"""
The status of a transfer.
"""

INITIATED = 0
"Transfer has been initiated, but client has not yet started moving data"
ONGOING = 1
"Client is currently (asynchronously) moving data to us. This is not possible with all transfer managers."
STAGED = 2
"Transfer has been staged, server is ready to complete the transfer."
COMPLETED = 3
"Transfer is completed"
FAILED = 4
"Transfer has been confirmed to have failed."
CANCELLED = 5
"Transfer has been cancelled by the client."
from alembic import op
from hera_librarian.deletion import DeletionPolicy
from hera_librarian.errors import ErrorCategory, ErrorSeverity
from hera_librarian.transfer import TransferStatus


def upgrade():
Expand Down Expand Up @@ -196,7 +161,19 @@ def upgrade():
# Securely store authenticator using a password hashing function
Column("authenticator", String(256), nullable=False),
Column("last_seen", DateTime(), nullable=False),
Column("last_heard", DateTime(), nullable=False)
Column("last_heard", DateTime(), nullable=False),
)

op.create_table(
"errors",
Column("id", Integer(), primary_key=True, autoincrement=True, unique=True),
Column("severity", Enum(ErrorSeverity), nullable=False),
Column("category", Enum(ErrorCategory), nullable=False),
Column("message", String, nullable=False),
Column("raised_time", DateTime(), nullable=False),
Column("cleared_time", DateTime()),
Column("cleared", Boolean(), nullable=False),
Column("caller", String(256)),
)


Expand Down
231 changes: 212 additions & 19 deletions hera_librarian/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
import sys
import time
from pathlib import Path
from typing import Optional

import dateutil.parser

from . import LibrarianClient
from .exceptions import LibrarianClientRemovedFunctionality, LibrarianError
from .exceptions import (LibrarianClientRemovedFunctionality, LibrarianError,
LibrarianHTTPError)
from .settings import client_settings

__version__ = "TEST"
Expand Down Expand Up @@ -53,6 +55,42 @@ def die(fmt, *args):
sys.exit(1)


def get_client(conn_name):
if conn_name not in client_settings.connections:
die("Connection name {} not found in client settings.".format(conn_name))

return LibrarianClient.from_info(client_settings.connections[conn_name])


def parse_create_time_window(
args,
start_time_name: str = "create_time_start",
end_time_name: str = "create_time_end",
) -> Optional[tuple[datetime.datetime, datetime.datetime]]:
"""
Parses a window to search for files between two times.
"""

create_time_window = None

if args.create_time_start is not None or args.create_time_end is not None:
create_time_window = []

if args.create_time_start is not None:
create_time_window.append(dateutil.parser.parse(args.create_time_start))
else:
create_time_window.append(datetime.datetime.min)

if args.create_time_end is not None:
create_time_window.append(dateutil.parser.parse(args.create_time_end))
else:
create_time_window.append(datetime.datetime.max)

create_time_window = tuple(create_time_window)

return create_time_window


# from https://stackoverflow.com/questions/17330139/python-printing-a-dictionary-as-a-horizontal-table-with-headers
def print_table(dict_list, col_list=None, col_names=None):
"""Pretty print a list of dictionaries as a dynamically sized table.
Expand Down Expand Up @@ -261,23 +299,7 @@ def search_files(args):

# Create the search request

# Start with the most complex part, parsing dates...
create_time_window = None

if args.create_time_start is not None or args.create_time_end is not None:
create_time_window = []

if args.create_time_start is not None:
create_time_window.append(dateutil.parser.parse(args.create_time_start))
else:
create_time_window.append(datetime.datetime.min)

if args.create_time_end is not None:
create_time_window.append(dateutil.parser.parse(args.create_time_end))
else:
create_time_window.append(datetime.datetime.max)

create_time_window = tuple(create_time_window)
create_time_window = parse_create_time_window(args)

# Perform the search

Expand Down Expand Up @@ -385,7 +407,76 @@ def upload(args):
except Exception as e:
die("Upload failed (unknown error): {}".format(e))

return
return 0


def search_errors(args):
"""
Search for errors on the librarian.
"""

client = get_client(args.conn_name)

create_time_window = parse_create_time_window(args)

try:
errors = client.search_errors(
id=args.id,
category=args.category,
severity=args.severity,
create_time_window=create_time_window,
include_resolved=args.include_resolved,
max_results=args.max_results,
)
except LibrarianHTTPError as e:
die(f"Unexpected error communicating with the librarian server: {e.reason}")

if len(errors) == 0:
print("No errors found.")
return

print_table(
[e.dict() for e in errors],
col_list=[
"id",
"severity",
"category",
"message",
"raised_time",
"cleared_time",
"cleared",
"caller",
],
col_names=[
"ID",
"Severity",
"Category",
"Message",
"Raised",
"Cleared",
"Cleared Time",
"Caller",
],
)

return 0


def clear_error(args):
"""
Clear an error on the librarian.
"""

client = get_client(args.conn_name)

try:
client.clear_error(args.id)
except ValueError as e:
die(f"Unable to find or clear error on the librarian: {e.args[0]}")
except LibrarianHTTPError as e:
die(f"Unexpected error communicating with the librarian server: {e.reason}")

return 0


# make the base parser
Expand Down Expand Up @@ -429,6 +520,8 @@ def generate_parser():
config_set_file_deletion_policy_subparser(sub_parsers)
config_stage_files_subparser(sub_parsers)
config_upload_subparser(sub_parsers)
config_search_errors_subparser(sub_parsers)
config_clear_error_subparser(sub_parsers)

return ap

Expand Down Expand Up @@ -940,6 +1033,106 @@ def config_upload_subparser(sub_parsers):
return


def config_search_errors_subparser(sub_parsers):
# function documentation
doc = """Search for errors in the librarian.
"""
example = """Search for errors matching the query, for instance to find all errors
with a level of 'CRITICAL', you would use:
librarian search-errors LIBRARIAN_NAME --severity=critical
"""

hlp = "Search for errors matching a query"

from .errors import ErrorCategory, ErrorSeverity

# add sub parser
sp = sub_parsers.add_parser(
"search-errors", description=doc, epilog=example, help=hlp
)

sp.add_argument("conn_name", metavar="CONNECTION-NAME", help=_conn_name_help)

sp.add_argument(
"--id",
help="Search for an error with this ID.",
type=int,
)

sp.add_argument(
"-c",
"--category",
type=ErrorCategory,
choices=list(ErrorCategory),
)

sp.add_argument(
"-s",
"--severity",
help="Search for errors with this severity.",
type=ErrorSeverity,
choices=list(ErrorSeverity),
)

sp.add_argument(
"--create-time-start",
help="Search for errors who were created after this date and time. Use a parseable date string, if no timezone is specified, UTC is assumed.",
)

sp.add_argument(
"--create-time-end",
help="Search for errors who were created before this date and time. Use a parseable date string, if no timezone is specified, UTC is assumed.",
)

sp.add_argument(
"--include-resolved",
action="store_true",
help="If this flag is present, include errors that have been cleared in the search. Otherwise, only active errors are returned.",
)

sp.add_argument(
"--max-results",
type=int,
default=64,
help="Maximum number of results to return.",
)

sp.set_defaults(func=search_errors)


def config_clear_error_subparser(sub_parsers):
# function documentation
doc = """Clear an error on the librarian.
"""
example = """Clear an error with the given ID:
librarian clear-error LIBRARIAN_NAME 1234
"""

hlp = "Clear an error on the librarian"

# add sub parser
sp = sub_parsers.add_parser(
"clear-error", description=doc, epilog=example, help=hlp
)

sp.add_argument("conn_name", metavar="CONNECTION-NAME", help=_conn_name_help)

sp.add_argument(
"id",
metavar="ERROR-ID",
help="The ID of the error to clear.",
type=int,
)

sp.set_defaults(func=clear_error)


def main():
# make a parser and run the specified command
parser = generate_parser()
Expand Down
Loading

0 comments on commit 083d6be

Please sign in to comment.