Skip to content

Commit 57e35c9

Browse files
kesmit13claude
andcommitted
refactor(functions): remove log_format config, add app_name parameter
- Remove external_function.log_format configuration option and --log-format CLI flag - Standardize on JSON formatted logging for all external function applications - Add external_function.app_name configuration option with --app-name CLI flag - Change Application class constructor parameter from 'name=' to 'app_name=' for consistency - Update logging configuration to use JSON formatter for both console and file output - Improve structured logging with additional context fields (app_name, request_id, etc.) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 0cd8d51 commit 57e35c9

File tree

3 files changed

+180
-88
lines changed

3 files changed

+180
-88
lines changed

singlestoredb/config.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -413,12 +413,6 @@
413413
environ=['SINGLESTOREDB_EXT_FUNC_LOG_FILE'],
414414
)
415415

416-
register_option(
417-
'external_function.log_format', 'string', check_str, '%(levelprefix)s %(message)s',
418-
'Log format string for formatting log messages.',
419-
environ=['SINGLESTOREDB_EXT_FUNC_LOG_FORMAT'],
420-
)
421-
422416
register_option(
423417
'external_function.name_prefix', 'string', check_str, '',
424418
'Prefix to add to external function names.',
@@ -468,6 +462,12 @@
468462
environ=['SINGLESTOREDB_EXT_FUNC_DISABLE_METRICS'],
469463
)
470464

465+
register_option(
466+
'external_function.app_name', 'string', check_str, None,
467+
'Name for the external function application instance.',
468+
environ=['SINGLESTOREDB_EXT_FUNC_APP_NAME'],
469+
)
470+
471471
#
472472
# Debugging options
473473
#

singlestoredb/functions/ext/asgi.py

Lines changed: 119 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@
9191

9292
logger = utils.get_logger('singlestoredb.functions.ext.asgi')
9393

94-
9594
# If a number of processes is specified, create a pool of workers
9695
num_processes = max(0, int(os.environ.get('SINGLESTOREDB_EXT_NUM_PROCESSES', 0)))
9796
if num_processes > 1:
@@ -688,16 +687,12 @@ class Application(object):
688687
File path to write logs to instead of console. If None, logs are
689688
written to console. When specified, application logger handlers
690689
are replaced with a file handler.
691-
log_format : str, optional
692-
Log format string for formatting log messages. Defaults to
693-
'%(levelprefix)s %(message)s'. Uses the DefaultFormatter which
694-
supports the %(levelprefix)s field.
695690
log_level : str, optional
696691
Logging level for the application logger. Valid values are 'info',
697692
'debug', 'warning', 'error'. Defaults to 'info'.
698693
disable_metrics : bool, optional
699694
Disable logging of function call metrics. Defaults to False.
700-
name : str, optional
695+
app_name : str, optional
701696
Name for the application instance. Used to create a logger-specific
702697
name. If not provided, a random name will be generated.
703698
@@ -867,10 +862,10 @@ def __init__(
867862
name_suffix: str = get_option('external_function.name_suffix'),
868863
function_database: Optional[str] = None,
869864
log_file: Optional[str] = get_option('external_function.log_file'),
870-
log_format: str = get_option('external_function.log_format'),
865+
871866
log_level: str = get_option('external_function.log_level'),
872867
disable_metrics: bool = get_option('external_function.disable_metrics'),
873-
name: Optional[str] = None,
868+
app_name: Optional[str] = get_option('external_function.app_name'),
874869
) -> None:
875870
if link_name and (link_config or link_credentials):
876871
raise ValueError(
@@ -888,10 +883,10 @@ def __init__(
888883
) or None
889884

890885
# Generate application name if not provided
891-
if name is None:
892-
name = f'ext_app_{secrets.token_hex(4)}'
886+
if app_name is None:
887+
app_name = f'udf_app_{secrets.token_hex(4)}'
893888

894-
self.name = name
889+
self.name = app_name
895890

896891
# Create logger instance specific to this application
897892
self.logger = utils.get_logger(f'singlestoredb.functions.ext.asgi.{self.name}')
@@ -988,33 +983,41 @@ def __init__(
988983
self.external_functions = external_functions
989984
self.function_database = function_database
990985
self.log_file = log_file
991-
self.log_format = log_format
992986
self.log_level = log_level
993987
self.disable_metrics = disable_metrics
994988

995989
# Configure logging
996990
self._configure_logging()
997991

998992
def _configure_logging(self) -> None:
999-
"""Configure logging based on the log_file and log_format settings."""
993+
"""Configure logging based on the log_file settings."""
1000994
# Set logger level
1001995
self.logger.setLevel(getattr(logging, self.log_level.upper()))
1002996

997+
# Remove all existing handlers to ensure clean configuration
998+
self.logger.handlers.clear()
999+
10031000
# Configure log file if specified
10041001
if self.log_file:
1005-
# Remove existing handlers
1006-
self.logger.handlers.clear()
1007-
10081002
# Create file handler
10091003
file_handler = logging.FileHandler(self.log_file)
10101004
file_handler.setLevel(getattr(logging, self.log_level.upper()))
10111005

1012-
# Create formatter
1013-
formatter = utils.DefaultFormatter(self.log_format)
1006+
# Use JSON formatter for file logging
1007+
formatter = utils.JSONFormatter()
10141008
file_handler.setFormatter(formatter)
10151009

10161010
# Add the handler to the logger
10171011
self.logger.addHandler(file_handler)
1012+
else:
1013+
# For console logging, create a new stream handler with JSON formatter
1014+
console_handler = logging.StreamHandler()
1015+
console_handler.setLevel(getattr(logging, self.log_level.upper()))
1016+
console_handler.setFormatter(utils.JSONFormatter())
1017+
self.logger.addHandler(console_handler)
1018+
1019+
# Prevent propagation to avoid duplicate or differently formatted messages
1020+
self.logger.propagate = False
10181021

10191022
async def __call__(
10201023
self,
@@ -1038,19 +1041,22 @@ async def __call__(
10381041
request_id = str(uuid.uuid4())
10391042

10401043
timer = Timer(
1044+
app_name=self.name,
10411045
id=request_id,
10421046
timestamp=datetime.datetime.now(
10431047
datetime.timezone.utc,
10441048
).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
10451049
)
10461050
call_timer = Timer(
1051+
app_name=self.name,
10471052
id=request_id,
10481053
timestamp=datetime.datetime.now(
10491054
datetime.timezone.utc,
10501055
).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
10511056
)
10521057

1053-
assert scope['type'] == 'http'
1058+
if scope['type'] != 'http':
1059+
raise ValueError(f"Expected HTTP scope, got {scope['type']}")
10541060

10551061
method = scope['method']
10561062
path = tuple(x for x in scope['path'].split('/') if x)
@@ -1077,13 +1083,14 @@ async def __call__(
10771083
if method == 'POST' and func is not None and path == self.invoke_path:
10781084

10791085
self.logger.info(
1080-
json.dumps({
1081-
'type': 'function_call',
1082-
'id': request_id,
1083-
'name': func_name.decode('utf-8'),
1086+
'Function call initiated',
1087+
extra={
1088+
'app_name': self.name,
1089+
'request_id': request_id,
1090+
'function_name': func_name.decode('utf-8'),
10841091
'content_type': content_type.decode('utf-8'),
10851092
'accepts': accepts.decode('utf-8'),
1086-
}),
1093+
},
10871094
)
10881095

10891096
args_data_format = func_info['args_data_format']
@@ -1164,7 +1171,13 @@ async def __call__(
11641171

11651172
except asyncio.TimeoutError:
11661173
self.logger.exception(
1167-
'Timeout in function call: ' + func_name.decode('utf-8'),
1174+
'Function call timeout',
1175+
extra={
1176+
'app_name': self.name,
1177+
'request_id': request_id,
1178+
'function_name': func_name.decode('utf-8'),
1179+
'timeout': func_info['timeout'],
1180+
},
11681181
)
11691182
body = (
11701183
'[TimeoutError] Function call timed out after ' +
@@ -1175,14 +1188,25 @@ async def __call__(
11751188

11761189
except asyncio.CancelledError:
11771190
self.logger.exception(
1178-
'Function call cancelled: ' + func_name.decode('utf-8'),
1191+
'Function call cancelled',
1192+
extra={
1193+
'app_name': self.name,
1194+
'request_id': request_id,
1195+
'function_name': func_name.decode('utf-8'),
1196+
},
11791197
)
11801198
body = b'[CancelledError] Function call was cancelled'
11811199
await send(self.error_response_dict)
11821200

11831201
except Exception as e:
11841202
self.logger.exception(
1185-
'Error in function call: ' + func_name.decode('utf-8'),
1203+
'Function call error',
1204+
extra={
1205+
'app_name': self.name,
1206+
'request_id': request_id,
1207+
'function_name': func_name.decode('utf-8'),
1208+
'exception_type': type(e).__name__,
1209+
},
11861210
)
11871211
body = f'[{type(e).__name__}] {str(e).strip()}'.encode('utf-8')
11881212
await send(self.error_response_dict)
@@ -1236,7 +1260,16 @@ async def __call__(
12361260
timer.metrics[k] = v
12371261

12381262
if not self.disable_metrics:
1239-
self.logger.info(json.dumps(timer.finish()))
1263+
metrics = timer.finish()
1264+
self.logger.info(
1265+
'Function call metrics',
1266+
extra={
1267+
'app_name': self.name,
1268+
'request_id': request_id,
1269+
'function_name': timer.metadata.get('function', ''),
1270+
'metrics': metrics,
1271+
},
1272+
)
12401273

12411274
def _create_link(
12421275
self,
@@ -1348,8 +1381,12 @@ def get_function_info(
13481381

13491382
except Exception as e:
13501383
self.logger.warning(
1351-
f'Could not parse docstring for function '
1352-
f'{key.decode("utf-8")}: {e}',
1384+
'Could not parse docstring for function',
1385+
extra={
1386+
'app_name': self.name,
1387+
'function_name': key.decode('utf-8'),
1388+
'error': str(e),
1389+
},
13531390
)
13541391

13551392
if not func_name or key == func_name:
@@ -1812,14 +1849,7 @@ def main(argv: Optional[List[str]] = None) -> None:
18121849
),
18131850
help='File path to write logs to instead of console',
18141851
)
1815-
parser.add_argument(
1816-
'--log-format', metavar='format',
1817-
default=defaults.get(
1818-
'log_format',
1819-
get_option('external_function.log_format'),
1820-
),
1821-
help='Log format string for formatting log messages',
1822-
)
1852+
18231853
parser.add_argument(
18241854
'--disable-metrics', action='store_true',
18251855
default=defaults.get(
@@ -1852,6 +1882,14 @@ def main(argv: Optional[List[str]] = None) -> None:
18521882
),
18531883
help='Database to use for the function definition',
18541884
)
1885+
parser.add_argument(
1886+
'--app-name', metavar='app_name',
1887+
default=defaults.get(
1888+
'app_name',
1889+
get_option('external_function.app_name'),
1890+
),
1891+
help='Name for the application instance',
1892+
)
18551893
parser.add_argument(
18561894
'functions', metavar='module.or.func.path', nargs='*',
18571895
help='functions or modules to export in UDF server',
@@ -1951,9 +1989,9 @@ def main(argv: Optional[List[str]] = None) -> None:
19511989
name_suffix=args.name_suffix,
19521990
function_database=args.function_database or None,
19531991
log_file=args.log_file,
1954-
log_format=args.log_format,
19551992
log_level=args.log_level,
19561993
disable_metrics=args.disable_metrics,
1994+
app_name=args.app_name,
19571995
)
19581996

19591997
funcs = app.get_create_functions(replace=args.replace_existing)
@@ -1965,7 +2003,7 @@ def main(argv: Optional[List[str]] = None) -> None:
19652003

19662004
try:
19672005
if args.db:
1968-
logger.info('registering functions with database')
2006+
logger.info('Registering functions with database')
19692007
app.register_functions(
19702008
args.db,
19712009
replace=args.replace_existing,
@@ -1980,43 +2018,48 @@ def main(argv: Optional[List[str]] = None) -> None:
19802018
).items() if v is not None
19812019
}
19822020

1983-
# Configure uvicorn logging to use the same log file if specified
1984-
if args.log_file:
1985-
log_config = {
1986-
'version': 1,
1987-
'disable_existing_loggers': False,
1988-
'formatters': {
1989-
'default': {
1990-
'()': 'singlestoredb.functions.ext.utils.DefaultFormatter',
1991-
'fmt': args.log_format,
1992-
},
2021+
# Configure uvicorn logging to use JSON format
2022+
log_config = {
2023+
'version': 1,
2024+
'disable_existing_loggers': False,
2025+
'formatters': {
2026+
'json': {
2027+
'()': 'singlestoredb.functions.ext.utils.JSONFormatter',
19932028
},
1994-
'handlers': {
1995-
'file': {
1996-
'class': 'logging.FileHandler',
1997-
'formatter': 'default',
1998-
'filename': args.log_file,
1999-
},
2029+
},
2030+
'handlers': {
2031+
'default': {
2032+
'class': (
2033+
'logging.FileHandler' if args.log_file
2034+
else 'logging.StreamHandler'
2035+
),
2036+
'formatter': 'json',
20002037
},
2001-
'loggers': {
2002-
'uvicorn': {
2003-
'handlers': ['file'],
2004-
'level': args.log_level.upper(),
2005-
'propagate': False,
2006-
},
2007-
'uvicorn.error': {
2008-
'handlers': ['file'],
2009-
'level': args.log_level.upper(),
2010-
'propagate': False,
2011-
},
2012-
'uvicorn.access': {
2013-
'handlers': ['file'],
2014-
'level': args.log_level.upper(),
2015-
'propagate': False,
2016-
},
2038+
},
2039+
'loggers': {
2040+
'uvicorn': {
2041+
'handlers': ['default'],
2042+
'level': args.log_level.upper(),
2043+
'propagate': False,
2044+
},
2045+
'uvicorn.error': {
2046+
'handlers': ['default'],
2047+
'level': args.log_level.upper(),
2048+
'propagate': False,
20172049
},
2018-
}
2019-
app_args['log_config'] = log_config
2050+
'uvicorn.access': {
2051+
'handlers': ['default'],
2052+
'level': args.log_level.upper(),
2053+
'propagate': False,
2054+
},
2055+
},
2056+
}
2057+
2058+
# Add filename to file handler if log file is specified
2059+
if args.log_file:
2060+
log_config['handlers']['default']['filename'] = args.log_file # type: ignore
2061+
2062+
app_args['log_config'] = log_config
20202063

20212064
if use_async:
20222065
asyncio.create_task(_run_uvicorn(uvicorn, app, app_args, db=args.db))
@@ -2025,7 +2068,7 @@ def main(argv: Optional[List[str]] = None) -> None:
20252068

20262069
finally:
20272070
if not use_async and args.db:
2028-
logger.info('dropping functions from database')
2071+
logger.info('Dropping functions from database')
20292072
app.drop_functions(args.db)
20302073

20312074

@@ -2038,7 +2081,7 @@ async def _run_uvicorn(
20382081
"""Run uvicorn server and clean up functions after shutdown."""
20392082
await uvicorn.Server(uvicorn.Config(app, **app_args)).serve()
20402083
if db:
2041-
logger.info('dropping functions from database')
2084+
logger.info('Dropping functions from database')
20422085
app.drop_functions(db)
20432086

20442087

0 commit comments

Comments
 (0)