2025年7月29日 星期二

Mapleboard MP510-50 測試 (三十六) : HTTP API 函式執行平台 (2)

在前一篇測試中已經勾勒出一個簡單的 HTTP API 函式執行平台雛形, 本篇主要是在此基礎上進行部分修改與功能擴充. 

本系列全部的測試紀錄參考 :


首先來修改主程式 serverless.py, 主要修改重點為 :
  • 改用 importlib.util.spec_from_file_location() 搭配 importlib.util.module_from_spec() 根據模組檔案的絕對路徑來動態載入模組, 此方式彈性較大, 可載入放在任何位置的模組檔案 (雖然我規劃固定放在 functions 子目錄下), 而且模組不需要加入 sys.path 中 (避免汙染系統路徑).
  • 新增錯誤日誌檔來紀錄錯誤與例外資訊以利查錯. 
關於用 importlib 動態載入模組方法參考 :
主程式修改如下 : 

# serverless.py
from flask import Flask, request, jsonify
import importlib.util
import os
import logging

app=Flask(__name__)
# 指定函式模組所在的資料夾
FUNCTIONS_DIR=os.path.expanduser('./functions')
# 指定錯誤日誌檔 (在目前工作目錄下)
logging.basicConfig(filename='serverless_error.log', level=logging.ERROR)
# 定義路由裝飾器 (支援 RESTful) 
# 若 URL=/function/hello : subpath 預設為 ''
# 若 URL=/function/hello/Tony :  subpath 為 'Tony'
@app.route('/function/<func_name>', defaults={'subpath': ''}, methods=['GET', 'POST'])
@app.route('/function/<func_name>/<path:subpath>', methods=['GET', 'POST'])
def handle_function(func_name, subpath):  # 傳入 subpath 支援 RESTful
    # 1. 取得檔案路徑
    func_path=os.path.join(FUNCTIONS_DIR, f'{func_name}.py')
    if not os.path.isfile(func_path):  # 模組檔案不存在 -> 回 404
        return jsonify({'error': f'Function "{func_name}" not found'}), 404
    try:
        # 2. 動態載入模組 (絕對路徑)
        spec=importlib.util.spec_from_file_location(func_name, func_path)
        module=importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        # 3. 檢查模組中有無 main() 函式 :
        if not hasattr(module, 'main'):  # 模組中無 main() 函式
            return jsonify({'error': f'Module "{func_name}" has no main()'}), 400
        # 4. 將 subpath 加入 request 中 (支援 RESTful)
        request.view_args['subpath']=subpath  
        # 5. 執行模組中的函式 :        
        result=module.main(request)  # 將 request 傳給被載入模組由其自行處理參數
        # 6. 傳回函式執行結果
        return result 
    except Exception as e:
        logging.exception(f'Error in function {func_name}\n{e}')  # 紀錄錯誤於日誌檔
        return jsonify({'error': 'Function execution failed'}), 500

此處藍色字體為修改或增加的部分, 利用 os.path.expanduser() 指定函式模組所在子目錄的方式更普適, 重點是動態載入模組的方式改為可從任何指定的絕對路徑載入且不用更動 sys.path 表. 與前一篇一樣把 subpath 加入 request 的 view_args 字典以支援 RESTful 網址路徑呼叫. 

測試用的函式模組 hello.py 與前一篇相同, 重抄如下 :

# hello.py
def main(request):
    name=request.args.get('name')
    if not name:
        # 從 RESTful 子路徑中抓變數 (如 /function/hello/Tony)
        subpath=request.view_args.get('subpath', '')
        parts=subpath.strip('/').split('/')
        if parts:
            name=parts[0]
    if not name:
        name='World'
    return f'Hello {name}!'

每次修改 Flask 主程式 serverless.py 後要重啟此程式的系統服務 (Nginx 不用) :

sudo systemctl restart serverless.service  

tony1966@LX2438:~/flask_apps/serverless$ sudo systemctl restart serverless.service   
[sudo] tony1966 的密碼: 

用瀏覽器訪問下列網址 : 





可見與前一篇的結果一樣. 

如果要查看錯誤日誌 serverless.err.log 可先用下列指令檢查檔案存不存在 :

ls -lh serverless_error.log   

tony1966@LX2438:~/flask_apps/serverless$ ls -lh serverless_error.log   
ls: 無法存取 'serverless_error.log': 沒有此一檔案或目錄

檔案存在的話再用 tail 或 cat 指令檢視日誌檔內容 :

tail serverless_error.log   (預設顯示最後 10 行)
tail -n 50 serverless_error.log  (檢視最後 50 行)
tail -f serverless_error.log  (debug 即時持續追蹤)
cat serverless_error.log  (檢視全部)
grep 'my_function' serverless_error.log  (篩選與 'my_function' 有關的)

注意, 只有等級為 ERROR 或更嚴重例如 CRITICAL 的錯誤訊息才會寫入 serverless_error.log, 一般 404 或 500 之類的錯誤不會被記錄, 除非修改函式模組 (例如 hello.py) 加入 404 與 500 的路由裝飾器, 呼叫 logging.error() 來記錄, 例如 :

@app.errorhandler(404)
def handle_404(e):
    logging.error(f'404 Not Found: {request.path}')
    return jsonify({'error': 'Not Found'}), 404

@app.errorhandler(500)
def handle_500(e):
    logging.error(f'500 Internal Server Error: {traceback.format_exc()}')
    return jsonify({'error': 'Internal Server Error'}), 500


2025-07-30 補充 :

今天在測試函式列表功能時出現錯誤, 被記錄到錯誤日誌檔 :

tony1966@LX2438:~/flask_apps/serverless$ tail serverless_error.log  
    '''.format(len(py_files))
KeyError: ' font-family'
ERROR:root:Error in function functions_list
' font-family'
Traceback (most recent call last):
  File "/home/tony1966/flask_apps/serverless/serverless.py", line 33, in handle_function
    result=module.main(request)  # 將 request 傳給被載入模組由其自行處理參數
  File "/home/tony1966/flask_apps/serverless/./functions/functions_list.py", line 43, in main
    '''.format(len(py_files))
KeyError: ' font-family'

原來是輸出網頁中的 CSS 大括號導致錯誤 :

            a { 
                text-decoration: none; 
                color: #007bff; 
                font-weight: bold; 
                }

用重複的大括號來跳脫就可以解決此錯誤了 :

            a {{ 
                text-decoration: none; 
                color: #007bff; 
                font-weight: bold; 
                }}

沒有留言 :