diff --git a/app/backend/app.py b/app/backend/app.py index 8611b0de5b..befa50f870 100644 --- a/app/backend/app.py +++ b/app/backend/app.py @@ -3,6 +3,7 @@ import mimetypes import time import logging +from logging.handlers import RotatingFileHandler import openai from flask import Flask, request, jsonify, send_file, abort from azure.identity import DefaultAzureCredential @@ -12,6 +13,12 @@ from approaches.readdecomposeask import ReadDecomposeAsk from approaches.chatreadretrieveread import ChatReadRetrieveReadApproach from azure.storage.blob import BlobServiceClient +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter +from opentelemetry.instrumentation.flask import FlaskInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor # Replace these with your own values, either in environment variables or directly here AZURE_STORAGE_ACCOUNT = os.environ.get("AZURE_STORAGE_ACCOUNT") or "mystorageaccount" @@ -23,6 +30,7 @@ AZURE_OPENAI_CHATGPT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_CHATGPT_DEPLOYMENT") or "chat" AZURE_OPENAI_CHATGPT_MODEL = os.environ.get("AZURE_OPENAI_CHATGPT_MODEL") or "gpt-35-turbo" AZURE_OPENAI_EMB_DEPLOYMENT = os.environ.get("AZURE_OPENAI_EMB_DEPLOYMENT") or "embedding" +AZURE_MONITOR_CONNECTION_STRING = os.environ.get("AZURE_MONITOR_CONNECTION_STRING") or "" KB_FIELDS_CONTENT = os.environ.get("KB_FIELDS_CONTENT") or "content" KB_FIELDS_CATEGORY = os.environ.get("KB_FIELDS_CATEGORY") or "category" @@ -71,6 +79,25 @@ KB_FIELDS_CONTENT) } +# Set up OpenTelemetry tracing +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer(__name__) + +# Set up Azure Monitor Trace Exporter +azure_exporter = AzureMonitorTraceExporter.from_connection_string(AZURE_MONITOR_CONNECTION_STRING) +span_processor = BatchSpanProcessor(azure_exporter) +trace.get_tracer_provider().add_span_processor(span_processor) + +# Set up logging +logging.basicConfig(level=logging.ERROR, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + RotatingFileHandler("app.log", maxBytes=100000, backupCount=10), + logging.StreamHandler() + ]) +logger = logging.getLogger(__name__) + + app = Flask(__name__) @app.route("/", defaults={"path": "index.html"}) @@ -96,36 +123,36 @@ def content_file(path): @app.route("/ask", methods=["POST"]) def ask(): - ensure_openai_token() - if not request.json: - return jsonify({"error": "request must be json"}), 400 - approach = request.json["approach"] - try: - impl = ask_approaches.get(approach) - if not impl: - return jsonify({"error": "unknown approach"}), 400 - r = impl.run(request.json["question"], request.json.get("overrides") or {}) - return jsonify(r) - except Exception as e: - logging.exception("Exception in /ask") - return jsonify({"error": str(e)}), 500 - + with tracer.start_as_current_span("ask"): + ensure_openai_token() + approach = request.json["approach"] + try: + impl = ask_approaches.get(approach) + if not impl: + app.logger.error("Unknown approach") + return jsonify({"error": "unknown approach"}), 400 + r = impl.run(request.json["question"], request.json.get("overrides") or {}) + return jsonify(r) + except Exception as e: + app.logger.error("Exception in /ask", exc_info=True) + return jsonify({"error": str(e)}), 500 + @app.route("/chat", methods=["POST"]) def chat(): - ensure_openai_token() - if not request.json: - return jsonify({"error": "request must be json"}), 400 - approach = request.json["approach"] - try: - impl = chat_approaches.get(approach) - if not impl: - return jsonify({"error": "unknown approach"}), 400 - r = impl.run(request.json["history"], request.json.get("overrides") or {}) - return jsonify(r) - except Exception as e: - logging.exception("Exception in /chat") - return jsonify({"error": str(e)}), 500 - + with tracer.start_as_current_span("chat"): + ensure_openai_token() + approach = request.json["approach"] + try: + impl = chat_approaches.get(approach) + if not impl: + app.logger.error("Unknown approach") + return jsonify({"error": "unknown approach"}), 400 + r = impl.run(request.json["history"], request.json.get("overrides") or {}) + return jsonify(r) + except Exception as e: + app.logger.error("Exception in /chat", exc_info=True) + return jsonify({"error": str(e)}), 500 + def ensure_openai_token(): global openai_token if openai_token.expires_on < int(time.time()) - 60: @@ -133,4 +160,5 @@ def ensure_openai_token(): openai.api_key = openai_token.token if __name__ == "__main__": + app.logger.setLevel(logging.INFO) app.run() diff --git a/app/backend/approaches/chatreadretrieveread.py b/app/backend/approaches/chatreadretrieveread.py index b57233fa5f..e48156a6a0 100644 --- a/app/backend/approaches/chatreadretrieveread.py +++ b/app/backend/approaches/chatreadretrieveread.py @@ -1,15 +1,49 @@ from typing import Any, Sequence - +import os +import logging +from logging.handlers import RotatingFileHandler import openai import tiktoken from azure.search.documents import SearchClient from azure.search.documents.models import QueryType from approaches.approach import Approach from text import nonewlines +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleExportSpanProcessor +from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter from core.messagebuilder import MessageBuilder from core.modelhelper import get_token_limit +AZURE_MONITOR_CONNECTION_STRING = os.environ.get("AZURE_MONITOR_CONNECTION_STRING") or "" + +# Set up Azure Monitor Trace Exporter +azure_exporter = AzureMonitorTraceExporter( + connection_string=AZURE_MONITOR_CONNECTION_STRING +) + +# Set the tracer_provider to the SDK's TracerProvider +trace.set_tracer_provider(TracerProvider()) + +# Configure the tracer_provider with the Azure exporter +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(azure_exporter) +) +# Get the tracer +tracer = trace.get_tracer(__name__) + +# Now you can use `tracer.start_as_current_span` to create spans + +# Set up logging +logging.basicConfig(level=logging.ERROR, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + RotatingFileHandler("app.log", maxBytes=100000, backupCount=10), + logging.StreamHandler() + ]) +logger = logging.getLogger(__name__) + class ChatReadRetrieveReadApproach(Approach): # Chat roles SYSTEM = "system" @@ -58,14 +92,15 @@ def __init__(self, search_client: SearchClient, chatgpt_deployment: str, chatgpt self.chatgpt_token_limit = get_token_limit(chatgpt_model) def run(self, history: Sequence[dict[str, str]], overrides: dict[str, Any]) -> Any: - has_text = overrides.get("retrieval_mode") in ["text", "hybrid", None] - has_vector = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] - use_semantic_captions = True if overrides.get("semantic_captions") and has_text else False - top = overrides.get("top") or 3 - exclude_category = overrides.get("exclude_category") or None - filter = "category ne '{}'".format(exclude_category.replace("'", "''")) if exclude_category else None + with tracer.start_as_current_span("ChatReadRetrieveReadApproach.run"): + has_text = overrides.get("retrieval_mode") in ["text", "hybrid", None] + has_vector = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] + use_semantic_captions = True if overrides.get("semantic_captions") and has_text else False + top = overrides.get("top") or 3 + exclude_category = overrides.get("exclude_category") or None + filter = "category ne '{}'".format(exclude_category.replace("'", "''")) if exclude_category else None - user_q = 'Generate search query for: ' + history[-1]["user"] + user_q = 'Generate search query for: ' + history[-1]["user"] # STEP 1: Generate an optimized keyword search query based on the chat history and the last question messages = self.get_messages_from_history( @@ -181,4 +216,4 @@ def get_messages_from_history(self, system_prompt: str, model_id: str, history: break messages = message_builder.messages - return messages \ No newline at end of file + return messages diff --git a/app/backend/requirements.txt b/app/backend/requirements.txt index 5620fbd7ba..f1412bba88 100644 --- a/app/backend/requirements.txt +++ b/app/backend/requirements.txt @@ -5,3 +5,9 @@ openai[datalib]==0.27.8 tiktoken==0.4.0 azure-search-documents==11.4.0b6 azure-storage-blob==12.14.1 +azure-monitor-opentelemetry --pre +azure-monitor-opentelemetry==1.0.0b13 +opentelemetry-instrumentation-requests +opentelemetry-instrumentation-flask +opentelemetry-instrumentation-wsgi +opentelemetry-sdk==1.17.0