@@ -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 :
0 commit comments