9191
9292logger = utils .get_logger ('singlestoredb.functions.ext.asgi' )
9393
94-
9594# If a number of processes is specified, create a pool of workers
9695num_processes = max (0 , int (os .environ .get ('SINGLESTOREDB_EXT_NUM_PROCESSES' , 0 )))
9796if 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