Skip to content

Commit ad74c9e

Browse files
kesmit13claude
andcommitted
feat(functions): refactor logging configuration for UDF applications
- Add log_file, log_format, and log_level parameters to Application class - Move logging configuration from main() to Application.__init__() via _configure_logging() - Add external_function.log_file and external_function.log_format config options - Update Application docstring to document all parameters including logging - Refactor Timer.finish() to return metrics dict instead of logging directly - Configure uvicorn logging to use same log file when specified This refactoring centralizes logging configuration and allows for more flexible UDF application logging through configuration parameters. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent f1b7255 commit ad74c9e

File tree

3 files changed

+120
-17
lines changed

3 files changed

+120
-17
lines changed

singlestoredb/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,18 @@
407407
environ=['SINGLESTOREDB_EXT_FUNC_LOG_LEVEL'],
408408
)
409409

410+
register_option(
411+
'external_function.log_file', 'string', check_str, None,
412+
'File path to write logs to instead of console.',
413+
environ=['SINGLESTOREDB_EXT_FUNC_LOG_FILE'],
414+
)
415+
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+
410422
register_option(
411423
'external_function.name_prefix', 'string', check_str, '',
412424
'Prefix to add to external function names.',

singlestoredb/functions/ext/asgi.py

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -678,8 +678,23 @@ class Application(object):
678678
link_credentials : Dict[str, Any], optional
679679
The CREDENTIALS section of a LINK definition. This dictionary gets
680680
converted to JSON for the CREATE LINK call.
681+
name_prefix : str, optional
682+
Prefix to add to function names when registering with the database
683+
name_suffix : str, optional
684+
Suffix to add to function names when registering with the database
681685
function_database : str, optional
682686
The database to use for external function definitions.
687+
log_file : str, optional
688+
File path to write logs to instead of console. If None, logs are
689+
written to console. When specified, application logger handlers
690+
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.
695+
log_level : str, optional
696+
Logging level for the application logger. Valid values are 'info',
697+
'debug', 'warning', 'error'. Defaults to 'info'.
683698
684699
"""
685700

@@ -846,6 +861,9 @@ def __init__(
846861
name_prefix: str = get_option('external_function.name_prefix'),
847862
name_suffix: str = get_option('external_function.name_suffix'),
848863
function_database: Optional[str] = None,
864+
log_file: Optional[str] = get_option('external_function.log_file'),
865+
log_format: str = get_option('external_function.log_format'),
866+
log_level: str = get_option('external_function.log_level'),
849867
) -> None:
850868
if link_name and (link_config or link_credentials):
851869
raise ValueError(
@@ -953,6 +971,33 @@ def __init__(
953971
self.endpoints = endpoints
954972
self.external_functions = external_functions
955973
self.function_database = function_database
974+
self.log_file = log_file
975+
self.log_format = log_format
976+
self.log_level = log_level
977+
978+
# Configure logging
979+
self._configure_logging()
980+
981+
def _configure_logging(self) -> None:
982+
"""Configure logging based on the log_file and log_format settings."""
983+
# Set logger level
984+
logger.setLevel(getattr(logging, self.log_level.upper()))
985+
986+
# Configure log file if specified
987+
if self.log_file:
988+
# Remove existing handlers
989+
logger.handlers.clear()
990+
991+
# Create file handler
992+
file_handler = logging.FileHandler(self.log_file)
993+
file_handler.setLevel(getattr(logging, self.log_level.upper()))
994+
995+
# Create formatter
996+
formatter = utils.DefaultFormatter(self.log_format)
997+
file_handler.setFormatter(formatter)
998+
999+
# Add the handler to the logger
1000+
logger.addHandler(file_handler)
9561001

9571002
async def __call__(
9581003
self,
@@ -1101,7 +1146,7 @@ async def __call__(
11011146
await send(output_handler['response'])
11021147

11031148
except asyncio.TimeoutError:
1104-
logging.exception(
1149+
logger.exception(
11051150
'Timeout in function call: ' + func_name.decode('utf-8'),
11061151
)
11071152
body = (
@@ -1112,14 +1157,14 @@ async def __call__(
11121157
await send(self.error_response_dict)
11131158

11141159
except asyncio.CancelledError:
1115-
logging.exception(
1160+
logger.exception(
11161161
'Function call cancelled: ' + func_name.decode('utf-8'),
11171162
)
11181163
body = b'[CancelledError] Function call was cancelled'
11191164
await send(self.error_response_dict)
11201165

11211166
except Exception as e:
1122-
logging.exception(
1167+
logger.exception(
11231168
'Error in function call: ' + func_name.decode('utf-8'),
11241169
)
11251170
body = f'[{type(e).__name__}] {str(e).strip()}'.encode('utf-8')
@@ -1173,7 +1218,7 @@ async def __call__(
11731218
for k, v in call_timer.metrics.items():
11741219
timer.metrics[k] = v
11751220

1176-
timer.finish()
1221+
logger.info(json.dumps(timer.finish()))
11771222

11781223
def _create_link(
11791224
self,
@@ -1740,6 +1785,22 @@ def main(argv: Optional[List[str]] = None) -> None:
17401785
),
17411786
help='logging level',
17421787
)
1788+
parser.add_argument(
1789+
'--log-file', metavar='filepath',
1790+
default=defaults.get(
1791+
'log_file',
1792+
get_option('external_function.log_file'),
1793+
),
1794+
help='File path to write logs to instead of console',
1795+
)
1796+
parser.add_argument(
1797+
'--log-format', metavar='format',
1798+
default=defaults.get(
1799+
'log_format',
1800+
get_option('external_function.log_format'),
1801+
),
1802+
help='Log format string for formatting log messages',
1803+
)
17431804
parser.add_argument(
17441805
'--name-prefix', metavar='name_prefix',
17451806
default=defaults.get(
@@ -1771,8 +1832,6 @@ def main(argv: Optional[List[str]] = None) -> None:
17711832

17721833
args = parser.parse_args(argv)
17731834

1774-
logger.setLevel(getattr(logging, args.log_level.upper()))
1775-
17761835
if i > 0:
17771836
break
17781837

@@ -1864,6 +1923,9 @@ def main(argv: Optional[List[str]] = None) -> None:
18641923
name_prefix=args.name_prefix,
18651924
name_suffix=args.name_suffix,
18661925
function_database=args.function_database or None,
1926+
log_file=args.log_file,
1927+
log_format=args.log_format,
1928+
log_level=args.log_level,
18671929
)
18681930

18691931
funcs = app.get_create_functions(replace=args.replace_existing)
@@ -1890,6 +1952,44 @@ def main(argv: Optional[List[str]] = None) -> None:
18901952
).items() if v is not None
18911953
}
18921954

1955+
# Configure uvicorn logging to use the same log file if specified
1956+
if args.log_file:
1957+
log_config = {
1958+
'version': 1,
1959+
'disable_existing_loggers': False,
1960+
'formatters': {
1961+
'default': {
1962+
'()': 'singlestoredb.functions.ext.utils.DefaultFormatter',
1963+
'fmt': args.log_format,
1964+
},
1965+
},
1966+
'handlers': {
1967+
'file': {
1968+
'class': 'logging.FileHandler',
1969+
'formatter': 'default',
1970+
'filename': args.log_file,
1971+
},
1972+
},
1973+
'loggers': {
1974+
'uvicorn': {
1975+
'handlers': ['file'],
1976+
'level': args.log_level.upper(),
1977+
'propagate': False,
1978+
},
1979+
'uvicorn.error': {
1980+
'handlers': ['file'],
1981+
'level': args.log_level.upper(),
1982+
'propagate': False,
1983+
},
1984+
'uvicorn.access': {
1985+
'handlers': ['file'],
1986+
'level': args.log_level.upper(),
1987+
'propagate': False,
1988+
},
1989+
},
1990+
}
1991+
app_args['log_config'] = log_config
1992+
18931993
if use_async:
18941994
asyncio.create_task(_run_uvicorn(uvicorn, app, app_args, db=args.db))
18951995
else:

singlestoredb/functions/ext/timer.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44
from typing import Dict
55
from typing import Optional
66

7-
from . import utils
8-
9-
logger = utils.get_logger('singlestoredb.functions.ext.metrics')
10-
117

128
class RoundedFloatEncoder(json.JSONEncoder):
139

@@ -87,12 +83,7 @@ def reset(self) -> None:
8783
self.entries.clear()
8884
self._current_key = None
8985

90-
def finish(self) -> None:
86+
def finish(self) -> Dict[str, Any]:
9187
"""Finish the current timing context and store the elapsed time."""
9288
self.metrics['total'] = time.perf_counter() - self.start_time
93-
self.log_metrics()
94-
95-
def log_metrics(self) -> None:
96-
if self.metadata.get('function'):
97-
result = dict(type='function_metrics', **self.metadata, **self.metrics)
98-
logger.info(json.dumps(result, cls=RoundedFloatEncoder))
89+
return dict(type='function_metrics', **self.metadata, **self.metrics)

0 commit comments

Comments
 (0)