diff --git a/application/single_app/app.py b/application/single_app/app.py index d8f837e33..ac56a87e3 100644 --- a/application/single_app/app.py +++ b/application/single_app/app.py @@ -321,6 +321,10 @@ def initialize_application(force=False): print("Setting up Application Insights logging...") setup_appinsights_logging(settings) logging.basicConfig(level=logging.DEBUG) + # basicConfig above is a no-op once Azure Monitor owns the root logger, + # and that same root handler stops Flask attaching its stderr handler, + # so unhandled tracebacks would otherwise never reach the container log. + ensure_console_error_logging(app.logger) ensure_default_global_agent_exists() start_background_tasks() diff --git a/application/single_app/config.py b/application/single_app/config.py index 0c548824e..3fc8cbf7d 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -96,7 +96,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.260.018" +VERSION = "0.260.019" IS_DEVELOPMENT = is_development_env_enabled() SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') diff --git a/application/single_app/functions_appinsights.py b/application/single_app/functions_appinsights.py index 4409556ab..05101ee29 100644 --- a/application/single_app/functions_appinsights.py +++ b/application/single_app/functions_appinsights.py @@ -3,6 +3,7 @@ import logging import os import re +import sys import threading from datetime import date, datetime from typing import Any, Dict, Optional, Tuple @@ -14,6 +15,8 @@ _appinsights_logger = None _azure_monitor_configured = False _logging_settings_load_state = threading.local() +CONSOLE_ERROR_HANDLER_ATTRIBUTE = "_simplechat_console_error_handler" +CONSOLE_ERROR_LOG_FORMAT = "[%(asctime)s] %(levelname)s in %(name)s: %(message)s" REDACTED_LOG_VALUE = "***REDACTED***" MAX_LOG_STRING_LENGTH = 8192 SENSITIVE_LOG_KEY_FRAGMENTS = ( @@ -629,3 +632,54 @@ def setup_appinsights_logging(settings): print(f"[AZURE_MONITOR] Failed to setup Application Insights: {e}") _azure_monitor_configured = False # Don't re-raise the exception, just continue without Application Insights + + +def ensure_console_error_logging(logger): + """ + Guarantee that a logger writes ERROR records to stderr. + + Flask only attaches its own stderr handler when ``logging.Logger`` finds no + level handler anywhere up the hierarchy. ``configure_azure_monitor`` attaches + a handler to the root logger, which also turns ``logging.basicConfig`` into a + no-op, so unhandled request tracebacks are delivered to Application Insights + and never printed. On App Service that leaves nothing but the access-log line + in the container log, which makes 500s effectively invisible. + + The handler is attached to the supplied logger rather than to root, and is + pinned at ERROR, so library DEBUG output is never echoed to the console. + + Args: + logger (logging.Logger): Logger to attach the stderr handler to, + typically ``app.logger``. + + Returns: + logging.Handler: The console handler now attached to the logger, or + ``None`` when no logger was supplied. + + Raises: + None: Handler setup failures are reported and swallowed so that logging + configuration can never prevent the application from starting. + """ + if logger is None: + return None + + try: + for existing_handler in logger.handlers: + if getattr(existing_handler, CONSOLE_ERROR_HANDLER_ATTRIBUTE, False): + return existing_handler + + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setLevel(logging.ERROR) + console_handler.setFormatter(logging.Formatter(CONSOLE_ERROR_LOG_FORMAT)) + setattr(console_handler, CONSOLE_ERROR_HANDLER_ATTRIBUTE, True) + logger.addHandler(console_handler) + + # A logger left at NOTSET defers to the root level, which Azure Monitor + # may have raised above ERROR, so pin it low enough to emit tracebacks. + if logger.level == logging.NOTSET or logger.level > logging.ERROR: + logger.setLevel(logging.ERROR) + + return console_handler + except Exception as handler_error: + print(f"[AZURE_MONITOR] Failed to attach console error handler: {handler_error}") + return None diff --git a/application/single_app/static/js/admin/admin_settings.js b/application/single_app/static/js/admin/admin_settings.js index 722a3d49f..55ee8fc7d 100644 --- a/application/single_app/static/js/admin/admin_settings.js +++ b/application/single_app/static/js/admin/admin_settings.js @@ -12,10 +12,8 @@ let imageSelected = window.imageSelected || []; let imageAll = window.imageAll || []; let classificationCategories = window.classificationCategories || []; -let enableDocumentClassification = window.enableDocumentClassification || false; let externalLinks = window.externalLinks || []; -let enableExternalLinks = window.enableExternalLinks || false; let externalLinksMenuName = window.externalLinksMenuName || 'External Links'; let agentsPagePromotedPopularAgents = Array.isArray(window.agentsPagePromotedPopularAgents) ? window.agentsPagePromotedPopularAgents diff --git a/application/single_app/templates/admin/_panes/actions.html b/application/single_app/templates/admin/_panes/actions.html index 221cfac5b..4f2a29f6d 100644 --- a/application/single_app/templates/admin/_panes/actions.html +++ b/application/single_app/templates/admin/_panes/actions.html @@ -1,4 +1,9 @@
+ {# These live here, not in the Agents pane, because sibling + {% include %} panes each render in their own scope: a value + set in one pane is not visible to the next one. #} + {% set analyze_capability = settings.document_action_capabilities.analyze %} + {% set comparison_capability = settings.document_action_capabilities.comparison %}
diff --git a/application/single_app/templates/admin/_panes/agents.html b/application/single_app/templates/admin/_panes/agents.html index 889caa9cc..86164afba 100644 --- a/application/single_app/templates/admin/_panes/agents.html +++ b/application/single_app/templates/admin/_panes/agents.html @@ -21,8 +21,6 @@ Note: All changes to global agents and actions require a restart of the web app to take effect.
- {% set analyze_capability = settings.document_action_capabilities.analyze %} - {% set comparison_capability = settings.document_action_capabilities.comparison %}
Agents Configuration diff --git a/application/single_app/templates/admin/_panes/cosmos.html b/application/single_app/templates/admin/_panes/cosmos.html index 6163be982..a3caaaa57 100644 --- a/application/single_app/templates/admin/_panes/cosmos.html +++ b/application/single_app/templates/admin/_panes/cosmos.html @@ -13,7 +13,7 @@
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %} @@ -27,7 +27,7 @@
Document access projection maintenance is automatic. The background scheduler repairs fail-open projection records first, then runs bounded backfill batches repeatedly while work remains. Production read metrics below show DAI-served reads, Redis cache hits, source fallbacks, RU, and latency without requiring shadow validation. - {% if enable_dai_debug %}Debug controls and shadow validation diagnostics are visible because enable_dai_debug is enabled in app settings.{% endif %} + {% if settings.enable_dai_debug %}Debug controls and shadow validation diagnostics are visible because enable_dai_debug is enabled in app settings.{% endif %}
@@ -57,7 +57,7 @@
Not loaded
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %}
Shadow Validation
@@ -91,7 +91,7 @@
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %}
Automatic Maintenance and Diagnostics
@@ -327,7 +327,7 @@
Default Read Path
Production read metrics are lightweight in-process counters for the current app worker. Application Insights logs remain the durable fleet-wide source for fallback warnings and query failures.
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %}
Last Shadow Result
@@ -653,7 +653,7 @@
- {% if enable_dai_debug %} + {% if settings.enable_dai_debug %}