import gc import logging import os from datadog import DogStatsd from ddtrace import tracer from flask import Flask, g, render_template, request, session from flask_cdn import CDN from flask_moment import Moment from htmlmin import minify from raven.contrib.flask import Sentry from raven.handlers.logging import SentryHandler from everyclass.server.utils import web_consts from everyclass.server.utils.common_helpers import plugin_available from everyclass.server.utils.session import EncryptedSessionInterface logger = logging.getLogger(__name__) sentry = Sentry() statsd = None __app = None try: import uwsgidecorators """ 使用 `uwsgidecorators.postfork` 装饰的函数会在 fork() 后的**每一个**子进程内被执行,执行顺序与这里的定义顺序一致 """ @uwsgidecorators.postfork def enable_gc(): """重新启用垃圾回收""" gc.set_threshold(700) @uwsgidecorators.postfork def init_plugins(): """初始化日志、错误追踪、打点插件""" from everyclass.rpc import init as init_rpc from everyclass.common.flask import print_config # Sentry if plugin_available("sentry"): sentry.init_app(app=__app) sentry_handler = SentryHandler(sentry.client) sentry_handler.setLevel(logging.WARNING) logging.getLogger().addHandler(sentry_handler) init_rpc(sentry=sentry) logger.info('Sentry is inited because you are in {} mode.'.format(__app.config['CONFIG_NAME'])) # metrics global statsd statsd = DogStatsd(namespace=f"{__app.config['SERVICE_NAME']}.{os.environ.get('MODE').lower()}", use_default_route=True) init_rpc(logger=logger) print_config(__app, logger) @uwsgidecorators.postfork def init_db(): """初始化数据库连接""" from everyclass.server.utils.db.postgres import init_pool as init_pg # init_mongo(__app) init_pg() @uwsgidecorators.postfork def fetch_remote_manifests(): """ 在 gevent 模式下,创建 Flask 对象时无法进行 HTTP 请求。因为此时 urllib2 是 gevent 补丁后的版本,而 gevent 引擎还没启动。 因此我们只能在 fork 后的每个进程中进行请求。 """ cron_update_remote_manifest() @uwsgidecorators.cron(0, -1, -1, -1, -1) def daily_update_data_time(signum): """每天凌晨更新数据最后更新时间""" cron_update_remote_manifest() except ModuleNotFoundError: pass def cron_update_remote_manifest(): """更新数据最后更新时间""" from everyclass.rpc.http import HttpRpc # 获取安卓客户端下载链接 android_manifest = HttpRpc.call(method="GET", url="https://everyclass.cdn.admirable.pro/android/manifest.json", retry=True) android_ver = android_manifest['latestVersions']['mainstream']['versionCode'] __app.config['ANDROID_CLIENT_URL'] = android_manifest['releases'][android_ver]['url'] # 更新数据最后更新时间 _api_server_status = HttpRpc.call(method="GET", url=__app.config['ENTITY_BASE_URL'] + '/info/service', retry=True, headers={'X-Auth-Token': __app.config['ENTITY_TOKEN']}) __app.config['DATA_LAST_UPDATE_TIME'] = _api_server_status["data"]["data_time"] def create_app() -> Flask: """创建 flask app""" from everyclass.server.utils.web_consts import MSG_INTERNAL_ERROR from everyclass.server import plugin_available from everyclass.server.utils import generate_error_response, api_helpers, base_exceptions from everyclass.common.env import is_production from everyclass.server.utils.web_consts import MSG_404 app = Flask(__name__, static_folder='../../frontend/dist', static_url_path='', template_folder="../../frontend/templates") # load app config from everyclass.server.utils.config import get_config _config = get_config() app.config.from_object(_config) # noqa: T484 """ 每课统一日志机制 规则如下: - DEBUG 模式下会输出 DEBUG 等级的日志,否则输出 INFO 及以上等级的日志 - 日志为 JSON 格式,会被节点上的 agent 采集并发送到 datadog,方便结合 metrics 和 APM 数据分析 - WARNING 以上级别的输出到 Sentry 做错误聚合 日志等级: critical – for errors that lead to termination error – for errors that occur, but are handled warning – for exceptional circumstances that might not be errors notice – for non-error messages you usually want to see info – for messages you usually don’t want to see debug – for debug messages Sentry: https://docs.sentry.io/clients/python/api/#raven.Client.captureMessage - stack 默认是 False """ if app.config['DEBUG']: logging.getLogger().setLevel(logging.DEBUG) else: logging.getLogger().setLevel(logging.INFO) # CDN CDN(app) # moment Moment(app) # encrypted session app.session_interface = EncryptedSessionInterface() # 导入并注册 blueprints from everyclass.server.calendar.views import calendar_bp from everyclass.server.calendar.views_api import calendar_api_bp from everyclass.server.entity.views import entity_bp from everyclass.server.entity.views_api import entity_api_bp from everyclass.server.user.views import user_bp from everyclass.server.user.views_api import user_api_bp from everyclass.server.course.views import course_bp from everyclass.server.course.views_api import course_api_bp from everyclass.server.views_main import main_blueprint app.register_blueprint(calendar_bp) app.register_blueprint(entity_bp) app.register_blueprint(user_bp, url_prefix='/user') app.register_blueprint(entity_api_bp, url_prefix='/mobile/entity') app.register_blueprint(user_api_bp, url_prefix='/mobile/user') app.register_blueprint(calendar_api_bp, url_prefix='/mobile/calendar') if 'course' in _config.FEATURE_GATING and _config.FEATURE_GATING['course']: app.register_blueprint(course_api_bp, url_prefix='/mobile/course') app.register_blueprint(course_bp, url_prefix='/course') app.register_blueprint(main_blueprint) # 初始化 RPC 模块 from everyclass.rpc.auth import Auth if 'AUTH_BASE_URL' in app.config: Auth.set_base_url(app.config['AUTH_BASE_URL']) @app.before_request def set_user_id(): """在请求之前设置 session uid,方便 APM 标识用户""" from everyclass.server.utils.web_consts import SESSION_CURRENT_USER, SESSION_USER_SEQ from everyclass.server.user import service as user_service if not session.get('user_id', None) and request.endpoint not in ("main.health_check", "static"): logger.info(f"Give a new user ID for new user. endpoint: {request.endpoint}") session['user_id'] = user_service.get_user_id() if session.get('user_id', None): tracer.current_root_span().set_tag("user_id", session[SESSION_USER_SEQ]) # 唯一用户 ID if session.get(SESSION_CURRENT_USER, None): tracer.current_root_span().set_tag("username", session[SESSION_CURRENT_USER].identifier) # 学号或教工号 @app.before_request def log_request(): """日志中记录请求""" logger.info(f'Request received: {request.method} {request.path}') @app.after_request def response_minify(response): """用 htmlmin 压缩 HTML,减轻带宽压力""" if app.config['HTML_MINIFY'] and response.content_type == u'text/html; charset=utf-8': response.set_data(minify(response.get_data(as_text=True))) return response from everyclass.server.utils.db.postgres import db_session @app.teardown_appcontext def shutdown_db_session(exception=None): db_session.remove() @app.template_filter('versioned') def version_filter(filename): """ 模板过滤器。如果 STATIC_VERSIONED,返回类似 'style-v1-c012dr.css' 的文件,而不是 'style-v1.css' :param filename: 文件名 :return: 新的文件名 """ if app.config['STATIC_VERSIONED']: if filename[:4] == 'css/': new_filename = app.config['STATIC_MANIFEST'][filename[4:]] return 'css/' + new_filename elif filename[:3] == 'js/': new_filename = app.config['STATIC_MANIFEST'][filename[3:]] return new_filename else: return app.config['STATIC_MANIFEST'][filename] return filename @app.context_processor def inject_consts(): """允许在模板中使用常量模块,以便使用session key等常量而不用在模板中硬编码""" return dict(consts=web_consts, api_base_url=app.config['MOBILE_API_BASE_URL']) @app.errorhandler(404) def page_not_found(error): if request.path.startswith('/mobile'): return generate_error_response(None, api_helpers.STATUS_CODE_INVALID_REQUEST, "no such API") return render_template('common/error.html', message=MSG_404) @app.errorhandler(base_exceptions.BizException) def handle_biz_exception(error: base_exceptions.BizException): if request.path.startswith("/mobile"): if isinstance(error, base_exceptions.InternalError): logger.error(repr(error)) # 业务错误的status_message可以对外展示 actual_error = {'status_message_overwrite': error.status_message} return generate_error_response(None, error.status_code, **actual_error) @app.errorhandler(500) def internal_server_error(error): if request.path.startswith("/mobile"): # 对于非业务错误,生产环境中不进行返回,其他环境中可返回 actual_error = { 'status_message_overwrite': f"server internal error: {repr(error.original_exception)}"} if not is_production() else {} return generate_error_response(None, api_helpers.STATUS_CODE_INTERNAL_ERROR, **actual_error) if plugin_available("sentry"): return render_template('common/error.html', message=MSG_INTERNAL_ERROR, event_id=g.sentry_event_id, public_dsn=sentry.client.get_public_dsn('https')) return f"<h4>500 Error: {repr(error.original_exception)}</h4><br>You are seeing this page because Sentry is not available." global __app __app = app return app