singlestoredb 1.15.1__cp38-abi3-macosx_10_9_universal2.whl → 1.15.2__cp38-abi3-macosx_10_9_universal2.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of singlestoredb might be problematic. Click here for more details.
- _singlestoredb_accel.abi3.so +0 -0
- singlestoredb/__init__.py +1 -1
- singlestoredb/apps/_python_udfs.py +18 -3
- singlestoredb/apps/_stdout_supress.py +1 -1
- singlestoredb/apps/_uvicorn_util.py +4 -0
- singlestoredb/config.py +18 -0
- singlestoredb/converters.py +1 -1
- singlestoredb/functions/ext/asgi.py +209 -23
- singlestoredb/functions/ext/timer.py +2 -11
- singlestoredb/functions/ext/utils.py +55 -6
- {singlestoredb-1.15.1.dist-info → singlestoredb-1.15.2.dist-info}/METADATA +1 -1
- {singlestoredb-1.15.1.dist-info → singlestoredb-1.15.2.dist-info}/RECORD +16 -16
- {singlestoredb-1.15.1.dist-info → singlestoredb-1.15.2.dist-info}/LICENSE +0 -0
- {singlestoredb-1.15.1.dist-info → singlestoredb-1.15.2.dist-info}/WHEEL +0 -0
- {singlestoredb-1.15.1.dist-info → singlestoredb-1.15.2.dist-info}/entry_points.txt +0 -0
- {singlestoredb-1.15.1.dist-info → singlestoredb-1.15.2.dist-info}/top_level.txt +0 -0
_singlestoredb_accel.abi3.so
CHANGED
|
Binary file
|
singlestoredb/__init__.py
CHANGED
|
@@ -13,6 +13,9 @@ if typing.TYPE_CHECKING:
|
|
|
13
13
|
# Keep track of currently running server
|
|
14
14
|
_running_server: 'typing.Optional[AwaitableUvicornServer]' = None
|
|
15
15
|
|
|
16
|
+
# Maximum number of UDFs allowed
|
|
17
|
+
MAX_UDFS_LIMIT = 10
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
async def run_udf_app(
|
|
18
21
|
log_level: str = 'error',
|
|
@@ -44,20 +47,32 @@ async def run_udf_app(
|
|
|
44
47
|
udf_suffix = ''
|
|
45
48
|
if app_config.running_interactively:
|
|
46
49
|
udf_suffix = '_test'
|
|
47
|
-
app = Application(
|
|
50
|
+
app = Application(
|
|
51
|
+
url=base_url,
|
|
52
|
+
app_mode='managed',
|
|
53
|
+
name_suffix=udf_suffix,
|
|
54
|
+
log_level=log_level,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if not app.endpoints:
|
|
58
|
+
raise ValueError('You must define at least one function.')
|
|
59
|
+
if len(app.endpoints) > MAX_UDFS_LIMIT:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f'You can only define a maximum of {MAX_UDFS_LIMIT} functions.',
|
|
62
|
+
)
|
|
48
63
|
|
|
49
64
|
config = uvicorn.Config(
|
|
50
65
|
app,
|
|
51
66
|
host='0.0.0.0',
|
|
52
67
|
port=app_config.listen_port,
|
|
53
|
-
|
|
68
|
+
log_config=app.get_uvicorn_log_config(),
|
|
54
69
|
)
|
|
55
|
-
_running_server = AwaitableUvicornServer(config)
|
|
56
70
|
|
|
57
71
|
# Register the functions only if the app is running interactively.
|
|
58
72
|
if app_config.running_interactively:
|
|
59
73
|
app.register_functions(replace=True)
|
|
60
74
|
|
|
75
|
+
_running_server = AwaitableUvicornServer(config)
|
|
61
76
|
asyncio.create_task(_running_server.serve())
|
|
62
77
|
await _running_server.wait_for_startup()
|
|
63
78
|
|
|
@@ -30,3 +30,7 @@ class AwaitableUvicornServer(uvicorn.Server):
|
|
|
30
30
|
|
|
31
31
|
async def wait_for_startup(self) -> None:
|
|
32
32
|
await self._startup_future
|
|
33
|
+
|
|
34
|
+
async def shutdown(self, sockets: Optional[list[socket.socket]] = None) -> None:
|
|
35
|
+
if self.started:
|
|
36
|
+
await super().shutdown(sockets)
|
singlestoredb/config.py
CHANGED
|
@@ -407,6 +407,12 @@ register_option(
|
|
|
407
407
|
environ=['SINGLESTOREDB_EXT_FUNC_LOG_LEVEL'],
|
|
408
408
|
)
|
|
409
409
|
|
|
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
|
+
|
|
410
416
|
register_option(
|
|
411
417
|
'external_function.name_prefix', 'string', check_str, '',
|
|
412
418
|
'Prefix to add to external function names.',
|
|
@@ -450,6 +456,18 @@ register_option(
|
|
|
450
456
|
environ=['SINGLESTOREDB_EXT_FUNC_TIMEOUT'],
|
|
451
457
|
)
|
|
452
458
|
|
|
459
|
+
register_option(
|
|
460
|
+
'external_function.disable_metrics', 'bool', check_bool, False,
|
|
461
|
+
'Disable logging of function call metrics.',
|
|
462
|
+
environ=['SINGLESTOREDB_EXT_FUNC_DISABLE_METRICS'],
|
|
463
|
+
)
|
|
464
|
+
|
|
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
|
+
|
|
453
471
|
#
|
|
454
472
|
# Debugging options
|
|
455
473
|
#
|
singlestoredb/converters.py
CHANGED
|
@@ -91,7 +91,6 @@ except ImportError:
|
|
|
91
91
|
|
|
92
92
|
logger = utils.get_logger('singlestoredb.functions.ext.asgi')
|
|
93
93
|
|
|
94
|
-
|
|
95
94
|
# If a number of processes is specified, create a pool of workers
|
|
96
95
|
num_processes = max(0, int(os.environ.get('SINGLESTOREDB_EXT_NUM_PROCESSES', 0)))
|
|
97
96
|
if num_processes > 1:
|
|
@@ -678,8 +677,24 @@ class Application(object):
|
|
|
678
677
|
link_credentials : Dict[str, Any], optional
|
|
679
678
|
The CREDENTIALS section of a LINK definition. This dictionary gets
|
|
680
679
|
converted to JSON for the CREATE LINK call.
|
|
680
|
+
name_prefix : str, optional
|
|
681
|
+
Prefix to add to function names when registering with the database
|
|
682
|
+
name_suffix : str, optional
|
|
683
|
+
Suffix to add to function names when registering with the database
|
|
681
684
|
function_database : str, optional
|
|
682
685
|
The database to use for external function definitions.
|
|
686
|
+
log_file : str, optional
|
|
687
|
+
File path to write logs to instead of console. If None, logs are
|
|
688
|
+
written to console. When specified, application logger handlers
|
|
689
|
+
are replaced with a file handler.
|
|
690
|
+
log_level : str, optional
|
|
691
|
+
Logging level for the application logger. Valid values are 'info',
|
|
692
|
+
'debug', 'warning', 'error'. Defaults to 'info'.
|
|
693
|
+
disable_metrics : bool, optional
|
|
694
|
+
Disable logging of function call metrics. Defaults to False.
|
|
695
|
+
app_name : str, optional
|
|
696
|
+
Name for the application instance. Used to create a logger-specific
|
|
697
|
+
name. If not provided, a random name will be generated.
|
|
683
698
|
|
|
684
699
|
"""
|
|
685
700
|
|
|
@@ -846,6 +861,10 @@ class Application(object):
|
|
|
846
861
|
name_prefix: str = get_option('external_function.name_prefix'),
|
|
847
862
|
name_suffix: str = get_option('external_function.name_suffix'),
|
|
848
863
|
function_database: Optional[str] = None,
|
|
864
|
+
log_file: Optional[str] = get_option('external_function.log_file'),
|
|
865
|
+
log_level: str = get_option('external_function.log_level'),
|
|
866
|
+
disable_metrics: bool = get_option('external_function.disable_metrics'),
|
|
867
|
+
app_name: Optional[str] = get_option('external_function.app_name'),
|
|
849
868
|
) -> None:
|
|
850
869
|
if link_name and (link_config or link_credentials):
|
|
851
870
|
raise ValueError(
|
|
@@ -862,6 +881,15 @@ class Application(object):
|
|
|
862
881
|
get_option('external_function.link_credentials') or '{}',
|
|
863
882
|
) or None
|
|
864
883
|
|
|
884
|
+
# Generate application name if not provided
|
|
885
|
+
if app_name is None:
|
|
886
|
+
app_name = f'udf_app_{secrets.token_hex(4)}'
|
|
887
|
+
|
|
888
|
+
self.name = app_name
|
|
889
|
+
|
|
890
|
+
# Create logger instance specific to this application
|
|
891
|
+
self.logger = utils.get_logger(f'singlestoredb.functions.ext.asgi.{self.name}')
|
|
892
|
+
|
|
865
893
|
# List of functions specs
|
|
866
894
|
specs: List[Union[str, Callable[..., Any], ModuleType]] = []
|
|
867
895
|
|
|
@@ -953,6 +981,97 @@ class Application(object):
|
|
|
953
981
|
self.endpoints = endpoints
|
|
954
982
|
self.external_functions = external_functions
|
|
955
983
|
self.function_database = function_database
|
|
984
|
+
self.log_file = log_file
|
|
985
|
+
self.log_level = log_level
|
|
986
|
+
self.disable_metrics = disable_metrics
|
|
987
|
+
|
|
988
|
+
# Configure logging
|
|
989
|
+
self._configure_logging()
|
|
990
|
+
|
|
991
|
+
def _configure_logging(self) -> None:
|
|
992
|
+
"""Configure logging based on the log_file settings."""
|
|
993
|
+
# Set logger level
|
|
994
|
+
self.logger.setLevel(getattr(logging, self.log_level.upper()))
|
|
995
|
+
|
|
996
|
+
# Remove all existing handlers to ensure clean configuration
|
|
997
|
+
self.logger.handlers.clear()
|
|
998
|
+
|
|
999
|
+
# Configure log file if specified
|
|
1000
|
+
if self.log_file:
|
|
1001
|
+
# Create file handler
|
|
1002
|
+
file_handler = logging.FileHandler(self.log_file)
|
|
1003
|
+
file_handler.setLevel(getattr(logging, self.log_level.upper()))
|
|
1004
|
+
|
|
1005
|
+
# Use JSON formatter for file logging
|
|
1006
|
+
formatter = utils.JSONFormatter()
|
|
1007
|
+
file_handler.setFormatter(formatter)
|
|
1008
|
+
|
|
1009
|
+
# Add the handler to the logger
|
|
1010
|
+
self.logger.addHandler(file_handler)
|
|
1011
|
+
else:
|
|
1012
|
+
# For console logging, create a new stream handler with JSON formatter
|
|
1013
|
+
console_handler = logging.StreamHandler()
|
|
1014
|
+
console_handler.setLevel(getattr(logging, self.log_level.upper()))
|
|
1015
|
+
console_handler.setFormatter(utils.JSONFormatter())
|
|
1016
|
+
self.logger.addHandler(console_handler)
|
|
1017
|
+
|
|
1018
|
+
# Prevent propagation to avoid duplicate or differently formatted messages
|
|
1019
|
+
self.logger.propagate = False
|
|
1020
|
+
|
|
1021
|
+
def get_uvicorn_log_config(self) -> Dict[str, Any]:
|
|
1022
|
+
"""
|
|
1023
|
+
Create uvicorn log config that matches the Application's logging format.
|
|
1024
|
+
|
|
1025
|
+
This method returns the log configuration used by uvicorn, allowing external
|
|
1026
|
+
users to match the logging format of the Application class.
|
|
1027
|
+
|
|
1028
|
+
Returns
|
|
1029
|
+
-------
|
|
1030
|
+
Dict[str, Any]
|
|
1031
|
+
Log configuration dictionary compatible with uvicorn's log_config parameter
|
|
1032
|
+
|
|
1033
|
+
"""
|
|
1034
|
+
log_config = {
|
|
1035
|
+
'version': 1,
|
|
1036
|
+
'disable_existing_loggers': False,
|
|
1037
|
+
'formatters': {
|
|
1038
|
+
'json': {
|
|
1039
|
+
'()': 'singlestoredb.functions.ext.utils.JSONFormatter',
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
'handlers': {
|
|
1043
|
+
'default': {
|
|
1044
|
+
'class': (
|
|
1045
|
+
'logging.FileHandler' if self.log_file
|
|
1046
|
+
else 'logging.StreamHandler'
|
|
1047
|
+
),
|
|
1048
|
+
'formatter': 'json',
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
'loggers': {
|
|
1052
|
+
'uvicorn': {
|
|
1053
|
+
'handlers': ['default'],
|
|
1054
|
+
'level': self.log_level.upper(),
|
|
1055
|
+
'propagate': False,
|
|
1056
|
+
},
|
|
1057
|
+
'uvicorn.error': {
|
|
1058
|
+
'handlers': ['default'],
|
|
1059
|
+
'level': self.log_level.upper(),
|
|
1060
|
+
'propagate': False,
|
|
1061
|
+
},
|
|
1062
|
+
'uvicorn.access': {
|
|
1063
|
+
'handlers': ['default'],
|
|
1064
|
+
'level': self.log_level.upper(),
|
|
1065
|
+
'propagate': False,
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
# Add filename to file handler if log file is specified
|
|
1071
|
+
if self.log_file:
|
|
1072
|
+
log_config['handlers']['default']['filename'] = self.log_file # type: ignore
|
|
1073
|
+
|
|
1074
|
+
return log_config
|
|
956
1075
|
|
|
957
1076
|
async def __call__(
|
|
958
1077
|
self,
|
|
@@ -976,19 +1095,22 @@ class Application(object):
|
|
|
976
1095
|
request_id = str(uuid.uuid4())
|
|
977
1096
|
|
|
978
1097
|
timer = Timer(
|
|
1098
|
+
app_name=self.name,
|
|
979
1099
|
id=request_id,
|
|
980
1100
|
timestamp=datetime.datetime.now(
|
|
981
1101
|
datetime.timezone.utc,
|
|
982
1102
|
).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
|
983
1103
|
)
|
|
984
1104
|
call_timer = Timer(
|
|
1105
|
+
app_name=self.name,
|
|
985
1106
|
id=request_id,
|
|
986
1107
|
timestamp=datetime.datetime.now(
|
|
987
1108
|
datetime.timezone.utc,
|
|
988
1109
|
).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
|
989
1110
|
)
|
|
990
1111
|
|
|
991
|
-
|
|
1112
|
+
if scope['type'] != 'http':
|
|
1113
|
+
raise ValueError(f"Expected HTTP scope, got {scope['type']}")
|
|
992
1114
|
|
|
993
1115
|
method = scope['method']
|
|
994
1116
|
path = tuple(x for x in scope['path'].split('/') if x)
|
|
@@ -1014,14 +1136,15 @@ class Application(object):
|
|
|
1014
1136
|
# Call the endpoint
|
|
1015
1137
|
if method == 'POST' and func is not None and path == self.invoke_path:
|
|
1016
1138
|
|
|
1017
|
-
logger.info(
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
'
|
|
1021
|
-
'
|
|
1139
|
+
self.logger.info(
|
|
1140
|
+
'Function call initiated',
|
|
1141
|
+
extra={
|
|
1142
|
+
'app_name': self.name,
|
|
1143
|
+
'request_id': request_id,
|
|
1144
|
+
'function_name': func_name.decode('utf-8'),
|
|
1022
1145
|
'content_type': content_type.decode('utf-8'),
|
|
1023
1146
|
'accepts': accepts.decode('utf-8'),
|
|
1024
|
-
}
|
|
1147
|
+
},
|
|
1025
1148
|
)
|
|
1026
1149
|
|
|
1027
1150
|
args_data_format = func_info['args_data_format']
|
|
@@ -1101,8 +1224,14 @@ class Application(object):
|
|
|
1101
1224
|
await send(output_handler['response'])
|
|
1102
1225
|
|
|
1103
1226
|
except asyncio.TimeoutError:
|
|
1104
|
-
|
|
1105
|
-
'
|
|
1227
|
+
self.logger.exception(
|
|
1228
|
+
'Function call timeout',
|
|
1229
|
+
extra={
|
|
1230
|
+
'app_name': self.name,
|
|
1231
|
+
'request_id': request_id,
|
|
1232
|
+
'function_name': func_name.decode('utf-8'),
|
|
1233
|
+
'timeout': func_info['timeout'],
|
|
1234
|
+
},
|
|
1106
1235
|
)
|
|
1107
1236
|
body = (
|
|
1108
1237
|
'[TimeoutError] Function call timed out after ' +
|
|
@@ -1112,15 +1241,26 @@ class Application(object):
|
|
|
1112
1241
|
await send(self.error_response_dict)
|
|
1113
1242
|
|
|
1114
1243
|
except asyncio.CancelledError:
|
|
1115
|
-
|
|
1116
|
-
'Function call cancelled
|
|
1244
|
+
self.logger.exception(
|
|
1245
|
+
'Function call cancelled',
|
|
1246
|
+
extra={
|
|
1247
|
+
'app_name': self.name,
|
|
1248
|
+
'request_id': request_id,
|
|
1249
|
+
'function_name': func_name.decode('utf-8'),
|
|
1250
|
+
},
|
|
1117
1251
|
)
|
|
1118
1252
|
body = b'[CancelledError] Function call was cancelled'
|
|
1119
1253
|
await send(self.error_response_dict)
|
|
1120
1254
|
|
|
1121
1255
|
except Exception as e:
|
|
1122
|
-
|
|
1123
|
-
'
|
|
1256
|
+
self.logger.exception(
|
|
1257
|
+
'Function call error',
|
|
1258
|
+
extra={
|
|
1259
|
+
'app_name': self.name,
|
|
1260
|
+
'request_id': request_id,
|
|
1261
|
+
'function_name': func_name.decode('utf-8'),
|
|
1262
|
+
'exception_type': type(e).__name__,
|
|
1263
|
+
},
|
|
1124
1264
|
)
|
|
1125
1265
|
body = f'[{type(e).__name__}] {str(e).strip()}'.encode('utf-8')
|
|
1126
1266
|
await send(self.error_response_dict)
|
|
@@ -1173,7 +1313,17 @@ class Application(object):
|
|
|
1173
1313
|
for k, v in call_timer.metrics.items():
|
|
1174
1314
|
timer.metrics[k] = v
|
|
1175
1315
|
|
|
1176
|
-
|
|
1316
|
+
if not self.disable_metrics:
|
|
1317
|
+
metrics = timer.finish()
|
|
1318
|
+
self.logger.info(
|
|
1319
|
+
'Function call metrics',
|
|
1320
|
+
extra={
|
|
1321
|
+
'app_name': self.name,
|
|
1322
|
+
'request_id': request_id,
|
|
1323
|
+
'function_name': timer.metadata.get('function', ''),
|
|
1324
|
+
'metrics': metrics,
|
|
1325
|
+
},
|
|
1326
|
+
)
|
|
1177
1327
|
|
|
1178
1328
|
def _create_link(
|
|
1179
1329
|
self,
|
|
@@ -1230,9 +1380,11 @@ class Application(object):
|
|
|
1230
1380
|
) -> Dict[str, Any]:
|
|
1231
1381
|
"""
|
|
1232
1382
|
Return the functions and function signature information.
|
|
1383
|
+
|
|
1233
1384
|
Returns
|
|
1234
1385
|
-------
|
|
1235
1386
|
Dict[str, Any]
|
|
1387
|
+
|
|
1236
1388
|
"""
|
|
1237
1389
|
functions = {}
|
|
1238
1390
|
no_default = object()
|
|
@@ -1284,8 +1436,13 @@ class Application(object):
|
|
|
1284
1436
|
doc_examples.append(ex_dict)
|
|
1285
1437
|
|
|
1286
1438
|
except Exception as e:
|
|
1287
|
-
logger.warning(
|
|
1288
|
-
|
|
1439
|
+
self.logger.warning(
|
|
1440
|
+
'Could not parse docstring for function',
|
|
1441
|
+
extra={
|
|
1442
|
+
'app_name': self.name,
|
|
1443
|
+
'function_name': key.decode('utf-8'),
|
|
1444
|
+
'error': str(e),
|
|
1445
|
+
},
|
|
1289
1446
|
)
|
|
1290
1447
|
|
|
1291
1448
|
if not func_name or key == func_name:
|
|
@@ -1740,6 +1897,22 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1740
1897
|
),
|
|
1741
1898
|
help='logging level',
|
|
1742
1899
|
)
|
|
1900
|
+
parser.add_argument(
|
|
1901
|
+
'--log-file', metavar='filepath',
|
|
1902
|
+
default=defaults.get(
|
|
1903
|
+
'log_file',
|
|
1904
|
+
get_option('external_function.log_file'),
|
|
1905
|
+
),
|
|
1906
|
+
help='File path to write logs to instead of console',
|
|
1907
|
+
)
|
|
1908
|
+
parser.add_argument(
|
|
1909
|
+
'--disable-metrics', action='store_true',
|
|
1910
|
+
default=defaults.get(
|
|
1911
|
+
'disable_metrics',
|
|
1912
|
+
get_option('external_function.disable_metrics'),
|
|
1913
|
+
),
|
|
1914
|
+
help='Disable logging of function call metrics',
|
|
1915
|
+
)
|
|
1743
1916
|
parser.add_argument(
|
|
1744
1917
|
'--name-prefix', metavar='name_prefix',
|
|
1745
1918
|
default=defaults.get(
|
|
@@ -1764,6 +1937,14 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1764
1937
|
),
|
|
1765
1938
|
help='Database to use for the function definition',
|
|
1766
1939
|
)
|
|
1940
|
+
parser.add_argument(
|
|
1941
|
+
'--app-name', metavar='app_name',
|
|
1942
|
+
default=defaults.get(
|
|
1943
|
+
'app_name',
|
|
1944
|
+
get_option('external_function.app_name'),
|
|
1945
|
+
),
|
|
1946
|
+
help='Name for the application instance',
|
|
1947
|
+
)
|
|
1767
1948
|
parser.add_argument(
|
|
1768
1949
|
'functions', metavar='module.or.func.path', nargs='*',
|
|
1769
1950
|
help='functions or modules to export in UDF server',
|
|
@@ -1771,8 +1952,6 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1771
1952
|
|
|
1772
1953
|
args = parser.parse_args(argv)
|
|
1773
1954
|
|
|
1774
|
-
logger.setLevel(getattr(logging, args.log_level.upper()))
|
|
1775
|
-
|
|
1776
1955
|
if i > 0:
|
|
1777
1956
|
break
|
|
1778
1957
|
|
|
@@ -1864,6 +2043,10 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1864
2043
|
name_prefix=args.name_prefix,
|
|
1865
2044
|
name_suffix=args.name_suffix,
|
|
1866
2045
|
function_database=args.function_database or None,
|
|
2046
|
+
log_file=args.log_file,
|
|
2047
|
+
log_level=args.log_level,
|
|
2048
|
+
disable_metrics=args.disable_metrics,
|
|
2049
|
+
app_name=args.app_name,
|
|
1867
2050
|
)
|
|
1868
2051
|
|
|
1869
2052
|
funcs = app.get_create_functions(replace=args.replace_existing)
|
|
@@ -1871,11 +2054,11 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1871
2054
|
raise RuntimeError('no functions specified')
|
|
1872
2055
|
|
|
1873
2056
|
for f in funcs:
|
|
1874
|
-
logger.info(f)
|
|
2057
|
+
app.logger.info(f)
|
|
1875
2058
|
|
|
1876
2059
|
try:
|
|
1877
2060
|
if args.db:
|
|
1878
|
-
logger.info('
|
|
2061
|
+
app.logger.info('Registering functions with database')
|
|
1879
2062
|
app.register_functions(
|
|
1880
2063
|
args.db,
|
|
1881
2064
|
replace=args.replace_existing,
|
|
@@ -1890,6 +2073,9 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1890
2073
|
).items() if v is not None
|
|
1891
2074
|
}
|
|
1892
2075
|
|
|
2076
|
+
# Configure uvicorn logging to use JSON format matching Application's format
|
|
2077
|
+
app_args['log_config'] = app.get_uvicorn_log_config()
|
|
2078
|
+
|
|
1893
2079
|
if use_async:
|
|
1894
2080
|
asyncio.create_task(_run_uvicorn(uvicorn, app, app_args, db=args.db))
|
|
1895
2081
|
else:
|
|
@@ -1897,7 +2083,7 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
1897
2083
|
|
|
1898
2084
|
finally:
|
|
1899
2085
|
if not use_async and args.db:
|
|
1900
|
-
logger.info('
|
|
2086
|
+
app.logger.info('Dropping functions from database')
|
|
1901
2087
|
app.drop_functions(args.db)
|
|
1902
2088
|
|
|
1903
2089
|
|
|
@@ -1910,7 +2096,7 @@ async def _run_uvicorn(
|
|
|
1910
2096
|
"""Run uvicorn server and clean up functions after shutdown."""
|
|
1911
2097
|
await uvicorn.Server(uvicorn.Config(app, **app_args)).serve()
|
|
1912
2098
|
if db:
|
|
1913
|
-
logger.info('
|
|
2099
|
+
app.logger.info('Dropping functions from database')
|
|
1914
2100
|
app.drop_functions(db)
|
|
1915
2101
|
|
|
1916
2102
|
|
|
@@ -4,10 +4,6 @@ from typing import Any
|
|
|
4
4
|
from typing import Dict
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
|
-
from . import utils
|
|
8
|
-
|
|
9
|
-
logger = utils.get_logger('singlestoredb.functions.ext.metrics')
|
|
10
|
-
|
|
11
7
|
|
|
12
8
|
class RoundedFloatEncoder(json.JSONEncoder):
|
|
13
9
|
|
|
@@ -87,12 +83,7 @@ class Timer:
|
|
|
87
83
|
self.entries.clear()
|
|
88
84
|
self._current_key = None
|
|
89
85
|
|
|
90
|
-
def finish(self) ->
|
|
86
|
+
def finish(self) -> Dict[str, Any]:
|
|
91
87
|
"""Finish the current timing context and store the elapsed time."""
|
|
92
88
|
self.metrics['total'] = time.perf_counter() - self.start_time
|
|
93
|
-
self.
|
|
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)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
|
+
import datetime
|
|
2
3
|
import json
|
|
3
4
|
import logging
|
|
4
5
|
import re
|
|
@@ -30,14 +31,62 @@ except ImportError:
|
|
|
30
31
|
return super().formatMessage(recordcopy)
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
class JSONFormatter(logging.Formatter):
|
|
35
|
+
"""Custom JSON formatter for structured logging."""
|
|
36
|
+
|
|
37
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
38
|
+
# Create proper ISO timestamp with microseconds
|
|
39
|
+
timestamp = datetime.datetime.fromtimestamp(
|
|
40
|
+
record.created, tz=datetime.timezone.utc,
|
|
41
|
+
)
|
|
42
|
+
# Keep only 3 digits for milliseconds
|
|
43
|
+
iso_timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
|
|
44
|
+
|
|
45
|
+
log_entry = {
|
|
46
|
+
'timestamp': iso_timestamp,
|
|
47
|
+
'level': record.levelname,
|
|
48
|
+
'logger': record.name,
|
|
49
|
+
'message': record.getMessage(),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Add extra fields if present
|
|
53
|
+
allowed_fields = [
|
|
54
|
+
'app_name', 'request_id', 'function_name',
|
|
55
|
+
'content_type', 'accepts', 'metrics',
|
|
56
|
+
]
|
|
57
|
+
for field in allowed_fields:
|
|
58
|
+
if hasattr(record, field):
|
|
59
|
+
log_entry[field] = getattr(record, field)
|
|
60
|
+
|
|
61
|
+
# Add exception info if present
|
|
62
|
+
if record.exc_info:
|
|
63
|
+
log_entry['exception'] = self.formatException(record.exc_info)
|
|
64
|
+
|
|
65
|
+
return json.dumps(log_entry)
|
|
66
|
+
|
|
67
|
+
|
|
33
68
|
def get_logger(name: str) -> logging.Logger:
|
|
34
|
-
"""Return a
|
|
69
|
+
"""Return a logger with JSON formatting."""
|
|
35
70
|
logger = logging.getLogger(name)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
|
|
72
|
+
# Only configure if not already configured with JSON formatter
|
|
73
|
+
has_json_formatter = any(
|
|
74
|
+
isinstance(getattr(handler, 'formatter', None), JSONFormatter)
|
|
75
|
+
for handler in logger.handlers
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not logger.handlers or not has_json_formatter:
|
|
79
|
+
# Clear handlers only if we need to reconfigure
|
|
80
|
+
logger.handlers.clear()
|
|
81
|
+
handler = logging.StreamHandler()
|
|
82
|
+
formatter = JSONFormatter()
|
|
83
|
+
handler.setFormatter(formatter)
|
|
84
|
+
logger.addHandler(handler)
|
|
85
|
+
logger.setLevel(logging.INFO)
|
|
86
|
+
|
|
87
|
+
# Prevent propagation to avoid duplicate messages or different formatting
|
|
88
|
+
logger.propagate = False
|
|
89
|
+
|
|
41
90
|
return logger
|
|
42
91
|
|
|
43
92
|
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
_singlestoredb_accel.abi3.so,sha256=
|
|
2
|
-
singlestoredb-1.15.1.dist-info/RECORD,,
|
|
3
|
-
singlestoredb-1.15.1.dist-info/LICENSE,sha256=Mlq78idURT-9G026aMYswwwnnrLcgzTLuXeAs5hjDLM,11341
|
|
4
|
-
singlestoredb-1.15.1.dist-info/WHEEL,sha256=_VEguvlLpUd-c8RbFMA4yMIVNMBv2LhpxYLCEQ-Bogk,113
|
|
5
|
-
singlestoredb-1.15.1.dist-info/entry_points.txt,sha256=bSLaTWB5zGjpVYPAaI46MkkDup0su-eb3uAhCNYuRV0,48
|
|
6
|
-
singlestoredb-1.15.1.dist-info/top_level.txt,sha256=lA65Vf4qAMfg_s1oG3LEO90h4t1Z-SPDbRqkevI3bSY,40
|
|
7
|
-
singlestoredb-1.15.1.dist-info/METADATA,sha256=PboWeUGiR17ArbHqeWX987eHZnBG6Q0M-eyugiRiBPI,5804
|
|
1
|
+
_singlestoredb_accel.abi3.so,sha256=uTb6s_nEjquymy6zozZ11x6GKRjE5c8vj9_3wbuvVGA,207216
|
|
8
2
|
sqlx/magic.py,sha256=JsS9_9aBFaOt91Torm1JPN0c8qB2QmYJmNSKtbSQIY0,3509
|
|
9
3
|
sqlx/__init__.py,sha256=aBYiU8DZXCogvWu3yWafOz7bZS5WWwLZXj7oL0dXGyU,85
|
|
4
|
+
singlestoredb-1.15.2.dist-info/RECORD,,
|
|
5
|
+
singlestoredb-1.15.2.dist-info/LICENSE,sha256=Mlq78idURT-9G026aMYswwwnnrLcgzTLuXeAs5hjDLM,11341
|
|
6
|
+
singlestoredb-1.15.2.dist-info/WHEEL,sha256=_VEguvlLpUd-c8RbFMA4yMIVNMBv2LhpxYLCEQ-Bogk,113
|
|
7
|
+
singlestoredb-1.15.2.dist-info/entry_points.txt,sha256=bSLaTWB5zGjpVYPAaI46MkkDup0su-eb3uAhCNYuRV0,48
|
|
8
|
+
singlestoredb-1.15.2.dist-info/top_level.txt,sha256=lA65Vf4qAMfg_s1oG3LEO90h4t1Z-SPDbRqkevI3bSY,40
|
|
9
|
+
singlestoredb-1.15.2.dist-info/METADATA,sha256=6-ReMW18GJNDWC_1ObD0oDwowWs9M6dcwN26DoV2gm4,5804
|
|
10
10
|
singlestoredb/auth.py,sha256=u8D9tpKzrqa4ssaHjyZnGDX1q8XBpGtuoOkTkSv7B28,7599
|
|
11
|
-
singlestoredb/config.py,sha256=
|
|
11
|
+
singlestoredb/config.py,sha256=aBdMrPEaNSH-QLi1AXoQaSJsZ9f6ZXoFPN-74Trr6sQ,13935
|
|
12
12
|
singlestoredb/vectorstore.py,sha256=BZb8e7m02_XVHqOyu8tA94R6kHb3n-BC8F08JyJwDzY,8408
|
|
13
|
-
singlestoredb/__init__.py,sha256=
|
|
13
|
+
singlestoredb/__init__.py,sha256=4XuKjp-JxKkJ0tjApI_BD6PPFGZvQNn0kGzz7rEy3Pw,2272
|
|
14
14
|
singlestoredb/types.py,sha256=Qp_PWYjSYG6PRnmXAZZ7K2QehUqfoG4KSllI3O1stPE,10397
|
|
15
15
|
singlestoredb/connection.py,sha256=ELk3-UpM6RaB993aIt08MydKiiDnejHQ1s8EFiacrAI,46055
|
|
16
16
|
singlestoredb/pytest.py,sha256=OyF3BO9mgxenifYhOihnzGk8WzCJ_zN5_mxe8XyFPOc,9074
|
|
17
17
|
singlestoredb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
singlestoredb/exceptions.py,sha256=HuoA6sMRL5qiCiee-_5ddTGmFbYC9Euk8TYUsh5GvTw,3234
|
|
19
|
-
singlestoredb/converters.py,sha256=
|
|
19
|
+
singlestoredb/converters.py,sha256=0D54e-3E2iVzlMYPK0RbGilE9B-kcP380c1Mpze2Nz4,20704
|
|
20
20
|
singlestoredb/fusion/graphql.py,sha256=ZA3HcDq5rER-dCEavwTqnF7KM0D2LCYIY7nLQk7lSso,5207
|
|
21
21
|
singlestoredb/fusion/handler.py,sha256=M5iyNP4zOaGqUqnZg_b5xhRE-8tHgfZSHDH0zKTiJmE,27692
|
|
22
22
|
singlestoredb/fusion/registry.py,sha256=jjdRTYZ3ylhy6gAoW5xBj0tkxGFBT-2yLQ0tztTgDIY,6112
|
|
@@ -153,11 +153,11 @@ singlestoredb/functions/__init__.py,sha256=I2GnxOhLb4_7xhgOxdIwmwD5NiK7QYPYaE3PU
|
|
|
153
153
|
singlestoredb/functions/dtypes.py,sha256=DgJaNXouJ2t-qIqDiQlUYU9IhkXXUTigWeE_MAcmvHM,39814
|
|
154
154
|
singlestoredb/functions/utils.py,sha256=1L0Phgzq0XdWK3ecfOOydq4zV955yCwpDoAaCYRGldk,10769
|
|
155
155
|
singlestoredb/functions/signature.py,sha256=h2vFVNP07d5a3gi7zMiM_sztDUNK_HlJWR-Rl3nMxPA,45545
|
|
156
|
-
singlestoredb/functions/ext/timer.py,sha256
|
|
157
|
-
singlestoredb/functions/ext/asgi.py,sha256=
|
|
156
|
+
singlestoredb/functions/ext/timer.py,sha256=-PR__KbhwAMW4PXJ4fGri2FfrU0jRyz6e6yvmySmjaw,2706
|
|
157
|
+
singlestoredb/functions/ext/asgi.py,sha256=H7YgSsqKzhVun9cj5iXvM_8yXM_Zciuy8BEPBp_dT4Y,71650
|
|
158
158
|
singlestoredb/functions/ext/arrow.py,sha256=WB7n1ACslyd8nlbFzUvlbxn1BVuEjA9-BGBEqCWlSOo,9061
|
|
159
159
|
singlestoredb/functions/ext/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
|
|
160
|
-
singlestoredb/functions/ext/utils.py,sha256=
|
|
160
|
+
singlestoredb/functions/ext/utils.py,sha256=oU2NVmkjcS0QHLfdB8SBiRylVq-r0VzTy8nxGvAgjow,6938
|
|
161
161
|
singlestoredb/functions/ext/mmap.py,sha256=RzyNSLRpI5ZJ8YN6k-AvZlRTLjj80j52byHLtW8c3ps,13710
|
|
162
162
|
singlestoredb/functions/ext/json.py,sha256=RIuZdDybEdHuC-f2p6BdjhFjM3iGb3a1PRQ4k11P6N8,10102
|
|
163
163
|
singlestoredb/functions/ext/rowdat_1.py,sha256=SlXbJ2042jEoaXw81y5llw1625w0aU2nZ8vI_O3qA-M,21112
|
|
@@ -169,13 +169,13 @@ singlestoredb/functions/typing/polars.py,sha256=b_UOIXLkvptHiAB7sXSzC7XPHMWNOglC
|
|
|
169
169
|
singlestoredb/notebook/__init__.py,sha256=v0j1E3MFAtaC8wTrR-F7XY0nytUvQ4XpYhVXddv2xA0,533
|
|
170
170
|
singlestoredb/notebook/_objects.py,sha256=MkB1eowEq5SQXFHY00xAKAyyeLqHu_uaZiA20BCJPaE,8043
|
|
171
171
|
singlestoredb/notebook/_portal.py,sha256=DLerIEQmAUymtYcx8RBeuYJ4pJSy_xl1K6t1Oc-eTf8,9698
|
|
172
|
-
singlestoredb/apps/_python_udfs.py,sha256=
|
|
172
|
+
singlestoredb/apps/_python_udfs.py,sha256=CwGt1ehR6CPvtUfLg8SK_ynXvvWHo_SeU_6xoVHQzys,3158
|
|
173
173
|
singlestoredb/apps/_dashboards.py,sha256=_03fI-GJannamA5lxLvIoC6Mim-H1jTRuI8-dw_P--k,1474
|
|
174
174
|
singlestoredb/apps/__init__.py,sha256=dfN97AZz7Np6JML3i9GJrv22ZbNCUletXmsJpQnKhKg,170
|
|
175
175
|
singlestoredb/apps/_cloud_functions.py,sha256=NJJu0uJsK9TjY3yZjgftpFPR-ga-FrOyaiDD4jWFCtE,2704
|
|
176
|
-
singlestoredb/apps/_uvicorn_util.py,sha256=
|
|
176
|
+
singlestoredb/apps/_uvicorn_util.py,sha256=tFcxd4XlPp_ULITN6aPi5MkPFRaEztD0HrbhBw0B1fk,1117
|
|
177
177
|
singlestoredb/apps/_process.py,sha256=G37fk6bzIxzhfEqp2aJBk3JCij-T2HFtTd078k5Xq9I,944
|
|
178
|
-
singlestoredb/apps/_stdout_supress.py,sha256=
|
|
178
|
+
singlestoredb/apps/_stdout_supress.py,sha256=wNL4YHEImqT3ptKsPPcolkCWN35vWxahEsi2rM7qpOY,665
|
|
179
179
|
singlestoredb/apps/_config.py,sha256=FlV0ABP7qlBJoKo9NOme6Fpp4yUFm5QEpHEHbl1A24o,2441
|
|
180
180
|
singlestoredb/apps/_connection_info.py,sha256=QOr-wcQJn6oCZw2kLEP0Uwzo85CGolGz0QIvlem3gug,303
|
|
181
181
|
singlestoredb/alchemy/__init__.py,sha256=dXRThusYrs_9GjrhPOw0-vw94in_T8yY9jE7SGCqiQk,2523
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|