ETF λ° μ£Όμ μ’ λͺ© λ°μ΄ν° λΆμ μμ΄μ νΈ νλ‘μ νΈ, μ£Όμ λ°μ΄ν°λ μ€μ λ°μ΄ν°μ΄μ§λ§ Application Insights μ λͺ¨λν°λ§ κΈ°λ₯ μμ°μ μν μμ μ λλ€.
sk-appinsights/
βββ src/ # Backend μμ€ μ½λ
β βββ agent/ # Semantic Kernel μμ΄μ νΈ
β β βββ __init__.py
β β βββ agent_service.py # μμ΄μ νΈ μλΉμ€
β β βββ stock_plugin.py # μ£Όμ λ°μ΄ν° νλ¬κ·ΈμΈ
β βββ api/ # FastAPI λΌμ°ν°
β β βββ __init__.py
β β βββ analytics.py # μ¬μ©μ νλ λΆμ API
β β βββ chat.py # AI μ±ν
API
β β βββ etf.py # ETF λ°μ΄ν° API
β β βββ news.py # λ΄μ€ API
β β βββ stocks.py # μ£Όμ λ°μ΄ν° API
β βββ observability/ # Application Insights ν
λ λ©νΈλ¦¬
β β βββ __init__.py
β β βββ middleware.py # HTTP μμ² μΆμ λ―Έλ€μ¨μ΄
β β βββ telemetry.py # ν
λ λ©νΈλ¦¬ μ€μ λ° μΆμ ν¨μ
β β βββ utils.py # μ νΈλ¦¬ν° ν¨μ
β βββ services/ # λΉμ¦λμ€ λ‘μ§ μλΉμ€
β β βββ __init__.py
β β βββ alphavantage_service.py # Alpha Vantage API
β β βββ cosmos_service.py # Cosmos DB μλΉμ€
β β βββ rss_news_service.py # RSS λ΄μ€ μλΉμ€
β β βββ totalrealreturns_service.py # TotalRealReturns API
β β βββ yfinance_service.py # Yahoo Finance API
β βββ __init__.py
β βββ config.py # μ€μ κ΄λ¦¬
β βββ main.py # FastAPI μλ² μ§μ
μ
β
βββ frontend/ # React λμ보λ
β βββ public/ # μ μ νμΌ
β β βββ index.html
β β βββ manifest.json
β β βββ robots.txt
β βββ src/
β β βββ components/ # React μ»΄ν¬λνΈ
β β β βββ ChatInterface.tsx # AI μ±ν
μΈν°νμ΄μ€
β β β βββ Dashboard.tsx # λ©μΈ λμ보λ
β β β βββ ETFList.tsx # ETF λͺ©λ‘
β β β βββ NewsFeed.tsx # λ΄μ€ νΌλ
β β β βββ StockDetail.tsx # μ£Όμ μμΈ μ 보
β β βββ hooks/ # 컀μ€ν
ν
β β β βββ usePageTracking.ts # νμ΄μ§ μΆμ ν
β β βββ services/ # API ν΄λΌμ΄μΈνΈ
β β β βββ analytics.ts # λΆμ API ν΄λΌμ΄μΈνΈ
β β β βββ api.ts # λ°±μλ API ν΄λΌμ΄μΈνΈ
β β βββ App.tsx # λ©μΈ μ± μ»΄ν¬λνΈ
β β βββ index.tsx # μ§μ
μ
β β βββ setupTests.ts # ν
μ€νΈ μ€μ
β βββ package.json # νλ‘ νΈμλ μμ‘΄μ±
β βββ tsconfig.json # TypeScript μ€μ
β
βββ .github/ # GitHub μ€μ
β βββ copilot-instructions.md # Copilot μ§μΉ¨
β
βββ .vscode/ # VSCode μ€μ
β βββ launch.json # λλ²κ·Έ μ€μ
β βββ tasks.json # νμ€ν¬ μ€μ
β
βββ λ¬Έμ/ # νλ‘μ νΈ λ¬Έμ
β βββ TELEMETRY_TABLES.md # ν
λ λ©νΈλ¦¬ ν
μ΄λΈ κ°μ΄λ
β βββ USER_BEHAVIOR_ANALYTICS.md # μ¬μ©μ νλ λΆμ κ°μ΄λ
β βββ LIVE_METRICS_GUIDE.md # Live Metrics κ°μ΄λ
β βββ DASHBOARD_SETUP.md # Azure λμ보λ μ€μ
β βββ COSMOS_DB_NETWORK_SETUP.md # Cosmos DB λ€νΈμν¬ μ€μ
β βββ GUIDE.md # κ°λ° κ°μ΄λ
β βββ WSL_NETWORK_SETUP.md # WSL λ€νΈμν¬ μ€μ
β
βββ ν
μ€νΈ νμΌ/
β βββ test_chat.py # μ±ν
API ν
μ€νΈ
β βββ test_cosmos.py # Cosmos DB ν
μ€νΈ
β βββ test_fallback.py # ν΄λ°± λ‘μ§ ν
μ€νΈ
β βββ test_live_metrics.py # Live Metrics ν
μ€νΈ
β βββ test_observability.py # ν
λ λ©νΈλ¦¬ ν
μ€νΈ
β βββ test_rss_news.py # RSS λ΄μ€ ν
μ€νΈ
β
βββ Azure μ€μ νμΌ/
β βββ azure-dashboard.json # Azure Portal λμ보λ
β βββ azure-workbook.json # Azure Workbook
β βββ *.example.json # μ€μ ν
νλ¦Ώ
β
βββ .env # νκ²½λ³μ (μ€μ κ°, git μ μΈ)
βββ .env.example # νκ²½λ³μ ν
νλ¦Ώ
βββ .gitignore # Git μ μΈ νμΌ
βββ pyproject.toml # Python νλ‘μ νΈ μ€μ (uv)
βββ uv.lock # μμ‘΄μ± μ κΈ νμΌ
βββ verify.sh # μμ€ν
κ²μ¦ μ€ν¬λ¦½νΈ
βββ LICENSE # λΌμ΄μ μ€
βββ README.md # νλ‘μ νΈ μ€λͺ
μ
- Python 3.13+: μ΅μ Python λ°νμ
- uv: κ³ μ Python ν¨ν€μ§ κ΄λ¦¬μ
- FastAPI: κ³ μ±λ₯ λΉλκΈ° REST API νλ μμν¬
- Uvicorn: ASGI μλ²
- Semantic Kernel 1.14+: Microsoft AI μμ΄μ νΈ νλ μμν¬
- μ£Όμ λ°μ΄ν° μμ€:
- yfinance: Yahoo Finance λ°μ΄ν° (μ£Όμ μμ€)
- Alpha Vantage API: 보쑰 λ°μ΄ν° μμ€
- TotalRealReturns API: ETF μμ΅λ₯ λ°μ΄ν°
- RSS Feeds: μ€μκ° λ΄μ€
- Azure Cosmos DB: NoSQL λ°μ΄ν°λ² μ΄μ€ (μΊμ± λ° μ μ₯)
- Application Insights: μ ν리μΌμ΄μ λͺ¨λν°λ§ λ° ν λ λ©νΈλ¦¬
- OpenTelemetry: λΆμ° μΆμ λ° λ©νΈλ¦ μμ§
azure-monitor-opentelemetry: Azure Monitor ν΅ν©opentelemetry-instrumentation-fastapi: FastAPI μλ κ³μΈ‘opentelemetry-instrumentation-httpx: HTTP ν΄λΌμ΄μΈνΈ μΆμ
- applicationinsights SDK: pageViews, customEvents μ μ‘
- React 18: μ¬μ©μ μΈν°νμ΄μ€ λΌμ΄λΈλ¬λ¦¬
- TypeScript: μ μ νμ JavaScript
- Material-UI (MUI): UI μ»΄ν¬λνΈ λΌμ΄λΈλ¬λ¦¬
- Axios: HTTP ν΄λΌμ΄μΈνΈ
- μ¬μ©μ νλ λΆμ: νμ΄μ§ λ·° μΆμ λ° μ΄λ²€νΈ λ‘κΉ
- π ETF μ’ λͺ© λͺ©λ‘: μ€μκ° ETF λ°μ΄ν° μ‘°ν
- π μ£Όμ μμΈ μ 보: κ°λ³ μ’ λͺ© λΆμ (κ°κ²©, κ±°λλ, λ΄μ€)
- π° λ΄μ€ νΌλ: RSS κΈ°λ° μ€μκ° μ£Όμ λ΄μ€
- π¬ AI μ±ν : Semantic Kernel κΈ°λ° μ£Όμ μ§μμλ΅
- π λ°μ΄ν° μκ°ν: μ°¨νΈ λ° κ·Έλν
- π μ¬μ©μ νλ λΆμ: Application Insights ν΅ν©
- π App Insights: KQL 쿼리λ₯Ό ν΅ν μ ν리μΌμ΄μ μ격 λΆμ λΆμ
- Python 3.13 μ΄μ
- Node.js 18 μ΄μ
- uv ν¨ν€μ§ κ΄λ¦¬μ
- Azure κ³μ (Application Insights, Cosmos DB)
# μ μ₯μ ν΄λ‘
git clone https://github.com/dotnetpower/sk-appinsights.git
cd sk-appinsights
# Python κ°μνκ²½ μμ± λ° νμ±ν
python3 -m venv .venv
source .venv/bin/activate # Linux/Mac
# .venv\Scripts\activate # Windows
# Python μμ‘΄μ± μ€μΉ
uv sync --prerelease=allow
# Frontend μμ‘΄μ± μ€μΉ
cd frontend
npm install
cd ...env νμΌμ μμ±νκ³ νμν κ°μ μ
λ ₯νμΈμ:
cp .env.example .envνμ νκ²½λ³μ:
# Deployment Environment
ENVIRONMENT=development # development, staging, production
# Azure Container Registry (λ°°ν¬μ©)
CONTAINER_REGISTRY_NAME=crskappinsights
RESOURCE_GROUP=rg-sk-appinsights
LOCATION=koreacentral
CONTAINER_APP_NAME=ca-sk-appinsights
# Application Insights (νμ)
APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=xxx;IngestionEndpoint=https://koreacentral-0.in.applicationinsights.azure.com/;LiveEndpoint=https://koreacentral.livediagnostics.monitor.azure.com/;ApplicationId=xxx"
APPLICATIONINSIGHTS_WORKSPACE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # KQL 쿼리μ©
# Azure Cosmos DB (νμ)
COSMOS_ENDPOINT="https://xxx.documents.azure.com:443/"
# Azure AD (RBAC) μΈμ¦ μ¬μ© μ - COSMOS_KEY μλ΅ κ°λ₯ (κΆμ₯)
# COSMOS_KEY="your-cosmos-key"
COSMOS_DATABASE_NAME="etf-agent"
COSMOS_CONTAINER_NAME="etf-data" # partition key = /symbol
COSMOS_ACCOUNT_NAME="cosmosskappinsights" # GitHub Actionsμ©
# AI μλΉμ€ - Azure OpenAI (κΆμ₯) λλ OpenAI
# μ΅μ
1: Azure OpenAI (κΆμ₯)
AZURE_OPENAI_ENDPOINT="https://xxx.openai.azure.com/"
AZURE_OPENAI_API_KEY="xxx"
AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
AZURE_OPENAI_API_VERSION="2024-08-01-preview"
# μ΅μ
2: OpenAI (Azure OpenAI μ¬μ© μ λΆνμ)
OPENAI_API_KEY="sk-xxx"
OPENAI_ORG_ID=""
# μ£Όμ λ°μ΄ν° API (μ νμ , yfinance fallback)
ALPHA_VANTAGE_KEY="your-alpha-vantage-key" # alphavantage.co
# FastAPI (λ‘컬 κ°λ°μ©)
API_HOST=0.0.0.0
API_PORT=8000
# React Frontend (λ‘컬 κ°λ°μ©)
REACT_APP_API_URL=http://localhost:8000- AI μλΉμ€: Azure OpenAI > OpenAI
- Azure OpenAIκ° μ€μ λμ΄ μμΌλ©΄ OPENAI_API_KEYλ 무μλ©λλ€
- Cosmos DB μΈμ¦: Azure AD (DefaultAzureCredential) > COSMOS_KEY
- Azure AD μΈμ¦ μ¬μ© μ COSMOS_KEYλ λΆνμν©λλ€ (κΆμ₯)
OPENAI_API_KEY: Azure OpenAI μ¬μ© μ λΆνμOPENAI_ORG_ID: OpenAI Organization μ¬μ© μμλ§ νμCOSMOS_KEY: Azure AD μΈμ¦ μ¬μ© μ λΆνμ (κΆμ₯)ALPHA_VANTAGE_KEY: yfinance μ°μ μ¬μ©, Alpha Vantageλ fallback
### 3. Backend μ€ν
```bash
# κ°μνκ²½ νμ±ν (μμ§ μ νλ€λ©΄)
source .venv/bin/activate
# λ°©λ² 1: UvicornμΌλ‘ μ§μ μ€ν (κ°λ° λͺ¨λ, μλ μ¬μμ)
uvicorn src.main:app --reload --host 0.0.0.0 --port 8000
# λ°©λ² 2: Python λͺ¨λλ‘ μ€ν
python -m src.main
# λ°©λ² 3: VSCode Task μ¬μ©
# Ctrl+Shift+B β "Backend: Start Server" μ ν
νμΈ:
- API μλ²: http://localhost:8000
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- Health Check: http://localhost:8000/health
cd frontend
# κ°λ° μλ² μμ
npm start
# λλ VSCode Task μ¬μ©
# Ctrl+Shift+B β "Frontend: Start Dev Server" μ ννμΈ:
- λμ보λ: http://localhost:3000
# VSCodeμμ Ctrl+Shift+B
# "Start All Services" μ ν β Backend + Frontend λμ μ€ν.env νμΌ
β
ββββ Python Backend (src/config.py)
β βββ os.getenv() β λͺ¨λ λ°±μλ μλΉμ€
β
ββββ React Frontend (process.env)
βββ REACT_APP_* β npm start β κ°λ° μλ²
.env νμΌ
β
ββββ setup-github-secrets.sh
β
βΌ
GitHub Secrets
β
ββββ GitHub Actions Workflow
β βββ Docker build-arg (Frontend λΉλ νμ)
β β βββ REACT_APP_VERSION, REACT_APP_GIT_COMMIT
β βββ az containerapp secret set
β
βΌ
Container App Secrets
β
ββββ Container Environment Variables
β
ββββ Running Container
βββ Python: os.getenv()
βββ React: λΉλ νμ μ£Όμ
κ° μ¬μ©
μ£Όμμ¬ν:
- React νκ²½λ³μλ λΉλ νμμ λ²λ€μ ν¬ν¨λ©λλ€
- Container Appμ λ°νμ νκ²½λ³μλ Reactμμ μ κ·Όν μ μμ΅λλ€
REACT_APP_API_URLμ λΉ κ°μΌλ‘ μ€μ νμ¬ μλκ²½λ‘λ₯Ό μ¬μ©νλ κ²μ κΆμ₯ν©λλ€
# ν¨ν€μ§ μΆκ°
uv add <package-name>
# κ°λ° μμ‘΄μ± μΆκ°
uv add --dev <package-name>
# μμ‘΄μ± λκΈ°ν
uv sync
# μ½λ ν¬λ§·ν
black src/
# λ¦°νΈ κ²μ¬
ruff check src/
# νμ
체ν¬
mypy src/
# ν
μ€νΈ μ€ν
pytest -v
# νΉμ ν
μ€νΈ μ€ν
python test_chat.py
python test_cosmos.pycd frontend
# ν¨ν€μ§ μΆκ°
npm install <package-name>
# κ°λ° μμ‘΄μ± μΆκ°
npm install --save-dev <package-name>
# λ¦°νΈ κ²μ¬
npm run lint
# λΉλ (νλ‘λμ
)
npm run build
# ν
μ€νΈ
npm testνλ‘μ νΈμλ λ€μ VSCode Tasksκ° μ€μ λμ΄ μμ΅λλ€:
- Backend: Start Server: Backend μλ² μ€ν
- Frontend: Start Dev Server: Frontend κ°λ° μλ² μ€ν
- Start All Services: Backend + Frontend λμ μ€ν (κΈ°λ³Έ)
- Python: Install Dependencies: uv sync μ€ν
- Python: Run Tests: pytest μ€ν
- Python: Format Code: black ν¬λ§·ν
- Python: Lint Code: ruff λ¦°νΈ
- Verify System: μμ€ν κ²μ¦ μ€ν¬λ¦½νΈ
μ€ν λ°©λ²: Ctrl+Shift+B β Task μ ν
.vscode/launch.jsonμ λλ²κ·Έ μ€μ ν¬ν¨λ¨F5ν€ λλ "Run and Debug" ν¨λ μ¬μ©- Breakpoint μ€μ κ°λ₯
# Live Metrics ν
μ€νΈ
python test_live_metrics.py
# ν
λ λ©νΈλ¦¬ ν
μ€νΈ
python test_observability.py
# Azure Portal β Application Insights β Logs
# KQL μΏΌλ¦¬λ‘ λ°μ΄ν° νμΈApplication Insightsμ Log Analytics workspaceμμλ νμ€ ν μ΄λΈλͺ μ μ¬μ©ν©λλ€. KQL 쿼리 μμ± μ λ€μ λ§€νμ μ°Έκ³ νμΈμ:
| κΈ°μ‘΄ ν μ΄λΈλͺ (Classic) | Log Analytics ν μ΄λΈλͺ | μ€λͺ | μ£Όμ μ»¬λΌ |
|---|---|---|---|
requests |
AppRequests |
HTTP μμ² μΆμ | TimeGenerated, Name, ResultCode, DurationMs, Success |
dependencies |
AppDependencies |
μΈλΆ μλΉμ€ νΈμΆ | TimeGenerated, Name, Type, Target, ResultCode, Success |
traces |
AppTraces |
λ‘κ·Έ λ©μμ§ | TimeGenerated, Message, SeverityLevel |
exceptions |
AppExceptions |
μμΈ λ° μ€λ₯ | TimeGenerated, ProblemId, OuterMessage, Type |
pageViews |
AppPageViews |
νμ΄μ§ λ·° μΆμ | TimeGenerated, Name, Url, DurationMs |
customEvents |
AppEvents |
컀μ€ν μ΄λ²€νΈ | TimeGenerated, Name, Properties |
customMetrics |
AppMetrics |
컀μ€ν λ©νΈλ¦ | TimeGenerated, Name, Sum, Count |
availabilityResults |
AppAvailabilityResults |
κ°μ©μ± ν μ€νΈ | TimeGenerated, Name, Success, DurationMs |
μ£Όμ μ»¬λΌ λ³κ²½μ¬ν:
timestampβTimeGeneratednameβNameresultCodeβResultCodedurationβDurationMssuccessβSuccesscustomDimensionsβPropertiescustomMeasurementsβMeasurements
KQL 쿼리 μμ:
// β μλͺ»λ 쿼리 (Classic ν
μ΄λΈλͺ
)
requests
| where timestamp > ago(1h)
| summarize count() by name
// β
μ¬λ°λ₯Έ 쿼리 (Log Analytics ν
μ΄λΈλͺ
)
AppRequests
| where TimeGenerated > ago(1h)
| summarize count() by Name
// 컀μ€ν
μ΄λ²€νΈ μ‘°ν
AppEvents
| where TimeGenerated > ago(24h)
| where Name == "page_view"
| extend page_name = tostring(Properties.page_name)
| summarize visit_count = count() by page_name
| order by visit_count desc
// μ±λ₯ λΆμ
AppRequests
| where TimeGenerated > ago(24h)
| summarize avg_duration = avg(DurationMs), request_count = count() by bin(TimeGenerated, 1h)
| render timechartApplication Insightsλ λ€μ 7κ°μ§ ν μ΄λΈμ μλ/μλμΌλ‘ λ°μ΄ν°λ₯Ό μμ§ν©λλ€:
μμ§ μμ : FastAPI HTTP μμ² μ²λ¦¬ μ
μμ§ λ°©μ: OpenTelemetry μλ κ³μΈ‘
μ μ₯ λ°μ΄ν°:
- name: "GET /api/etf", "POST /api/chat"
- url: μ 체 μμ² URL
- duration: μμ² μ²λ¦¬ μκ° (λ°λ¦¬μ΄)
- resultCode: HTTP μν μ½λ (200, 404, 500)
- success: μ±κ³΅/μ€ν¨ μ¬λΆ
- customDimensions: μμ² νλΌλ―Έν°, ν€λ λ±
μμ:
# FastAPI μλν¬μΈνΈ νΈμΆ μ μλ κΈ°λ‘
@app.get("/api/etf")
async def get_etf_list():
# μ΄ ν¨μκ° νΈμΆλλ©΄ requests ν
μ΄λΈμ μλ μ μ₯
passμμ§ μμ : μΈλΆ API νΈμΆ, DB 쿼리 μ€ν μ
μμ§ λ°©μ: HTTPX, Cosmos DB SDK μλ κ³μΈ‘
μ μ₯ λ°μ΄ν°:
- name: API νΈμΆ μ΄λ¦
- type: "HTTP", "Azure Cosmos DB"
- target: λμ μλ²/μλΉμ€
- data: SQL 쿼리, API URL
- duration: νΈμΆ μκ° (λ°λ¦¬μ΄)
- success: μ±κ³΅/μ€ν¨
- resultCode: μλ΅ μ½λ
μμ:
# yfinance API νΈμΆ μ μλ κΈ°λ‘ (HTTPX κ³μΈ‘)
import httpx
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/stock")
# dependencies ν
μ΄λΈμ μλ μ μ₯
# Cosmos DB 쿼리 μ μλ κΈ°λ‘
container.query_items(
query="SELECT * FROM c WHERE c.type = @type",
parameters=[{"name": "@type", "value": "ETF"}]
)
# dependencies ν
μ΄λΈμ μλ μ μ₯μμ§ μμ : Python logger μ¬μ© μ
μμ§ λ°©μ: Python logging μλ μ°λ
μ μ₯ λ°μ΄ν°:
- message: λ‘κ·Έ λ©μμ§
- severityLevel: 0=Verbose, 1=Info, 2=Warning, 3=Error, 4=Critical
- timestamp: λ‘κ·Έ λ°μ μκ°
- customDimensions: μΆκ° 컨ν
μ€νΈ
μμ:
import logging
logger = logging.getLogger(__name__)
# λͺ¨λ λ‘κ·Έκ° traces ν
μ΄λΈμ μλ μ μ₯
logger.info("ETF λ°μ΄ν° μ‘°ν μμ") # severityLevel=1
logger.warning("μΊμ λ§λ£λ¨") # severityLevel=2
logger.error("API νΈμΆ μ€ν¨") # severityLevel=3μμ§ μμ : track_page_view() ν¨μ νΈμΆ μ
μμ§ λ°©μ: TelemetryClient λͺ
μμ νΈμΆ
μ μ₯ λ°μ΄ν°:
- name: νμ΄μ§ μ΄λ¦ ("Dashboard", "ETF List")
- url: νμ΄μ§ URL
- customDimensions.duration_ms: νμ΄μ§ 체λ₯ μκ° (λ°λ¦¬μ΄)
- customDimensions.user_id: μ¬μ©μ ID
- customDimensions.session_id: μΈμ
ID
μμ:
# Backend API
from src.observability.telemetry import track_page_view
@router.post("/api/analytics/page-view")
async def log_page_view(event: PageViewEvent):
track_page_view(
name=event.page_name,
url=f"/{event.page_name}",
properties={"user_id": event.user_id, "session_id": event.session_id},
duration_ms=event.duration_ms
)
# pageViews ν
μ΄λΈμ μ μ₯Frontend ν΅ν©:
// React μ»΄ν¬λνΈ λ§μ΄νΈ/μΈλ§μ΄νΈ μ
useEffect(() => {
const entryTime = Date.now();
return () => {
const duration = Date.now() - entryTime;
trackPageView({
page_name: "Dashboard",
duration_ms: duration,
user_id: getUserId(),
session_id: getSessionId()
});
};
}, []);μμ§ μμ : track_user_event() ν¨μ νΈμΆ μ
μμ§ λ°©μ: TelemetryClient λͺ
μμ νΈμΆ
μ μ₯ λ°μ΄ν°:
- name: μ΄λ²€νΈ μ΄λ¦ ("button_click", "search", "tab_changed")
- customDimensions.event_category: μ΄λ²€νΈ μΉ΄ν
κ³ λ¦¬
- customDimensions.user_id: μ¬μ©μ ID
- customDimensions.*: μ΄λ²€νΈλ³ μΆκ° μμ±
- customMeasurements: μ«μ μΈ‘μ κ°
μμ:
# Backend API
from src.observability.telemetry import track_user_event
@router.post("/api/analytics/event")
async def log_user_event(event: UserEvent):
track_user_event(
name=event.event_name,
properties={
"event_category": event.event_category,
"user_id": event.user_id,
"query": event.query # κ²μ μ΄λ²€νΈμ κ²½μ°
}
)
# customEvents ν
μ΄λΈμ μ μ₯Frontend μ΄λ²€νΈ μΆμ :
// ν λ³κ²½ μ
trackEvent({
event_name: "tab_changed",
event_category: "navigation",
properties: { from_tab: "Dashboard", to_tab: "ETF List" }
});
// κ²μ μ
trackEvent({
event_name: "search",
event_category: "interaction",
properties: { query: "AAPL", results_count: 5 }
});μμ§ μμ : OpenTelemetry Metrics κΈ°λ‘ μ
μμ§ λ°©μ: Meter API μ¬μ©
μ μ₯ λ°μ΄ν°:
- name: λ©νΈλ¦ μ΄λ¦ ("app.requests.total", "app.page_views.duration")
- value: λ©νΈλ¦ κ°
- valueCount: μΈ‘μ νμ
- valueSum: ν©κ³
- customDimensions: λ©νΈλ¦ μμ± (page_name, endpoint λ±)
μμ:
# μ΄κΈ°ν μ λ©νΈλ¦ μμ±
from src.observability.telemetry import initialize_metrics
initialize_metrics() # μ± μμ μ ν λ² νΈμΆ
# μλμΌλ‘ λ€μ λ©νΈλ¦ μμ§:
# - app.requests.total: μμ² μΉ΄μ΄ν°
# - app.requests.duration: μμ² μ²λ¦¬ μκ° νμ€ν κ·Έλ¨
# - app.errors.total: μλ¬ μΉ΄μ΄ν°
# - app.page_views.total: νμ΄μ§ λ·° μΉ΄μ΄ν°
# - app.page_views.duration: νμ΄μ§ 체λ₯ μκ° νμ€ν κ·Έλ¨
# - app.user_events.total: μ¬μ©μ μ΄λ²€νΈ μΉ΄μ΄ν°μμ§ μμ : μμΈ λ°μ μ λλ track_exception() νΈμΆ μ
μμ§ λ°©μ: OpenTelemetry span.record_exception() μλ + μλ νΈμΆ
μ μ₯ λ°μ΄ν°:
- type: μμΈ νμ
(ValueError, HTTPException)
- outerMessage: μμΈ λ©μμ§
- problemId: κ°μ μμΈ κ·Έλ£Ήν ID
- customDimensions: μμΈ λ°μ 컨ν
μ€νΈ (endpoint, user_id λ±)
μμ:
# μλ μμ§ - μ²λ¦¬λμ§ μμ μμΈ
@app.get("/api/data")
async def get_data():
result = 10 / 0 # ZeroDivisionError μλ κΈ°λ‘
# μλ μμ§ - λͺ
μμ μΆμ
from src.observability.telemetry import track_exception
try:
risky_operation()
except Exception as e:
track_exception(e, {
"operation": "risky_operation",
"user_id": current_user_id
})
raise# src/main.py
from src.observability.telemetry import setup_telemetry, initialize_metrics
app = FastAPI()
# 1. ν
λ λ©νΈλ¦¬ μ€μ
setup_telemetry(app)
# - FastAPI μλ κ³μΈ‘ νμ±ν (requests)
# - HTTPX μλ κ³μΈ‘ νμ±ν (dependencies)
# - Cosmos DB μλ κ³μΈ‘ νμ±ν (dependencies)
# - TelemetryClient μ΄κΈ°ν (pageViews, customEvents)
# 2. 컀μ€ν
λ©νΈλ¦ μ΄κΈ°ν
initialize_metrics()
# - customMetrics ν
μ΄λΈμ© Meter μμ±1. μμ² μμ
β
2. requests ν
μ΄λΈμ μλ κΈ°λ‘ (OpenTelemetry)
β
3. λ―Έλ€μ¨μ΄μμ μ²λ¦¬ μκ° μΈ‘μ
β
4. μΈλΆ API νΈμΆ μ dependencies ν
μ΄λΈμ μλ κΈ°λ‘
β
5. logger μ¬μ© μ traces ν
μ΄λΈμ μλ κΈ°λ‘
β
6. μμΈ λ°μ μ exceptions ν
μ΄λΈμ μλ κΈ°λ‘
β
7. μλ΅ λ°ν
1. μ¬μ©μκ° νμ΄μ§ λ°©λ¬Έ
β
2. React useEffect ν
μ€ν
β
3. νμ΄μ§ μ§μ
μκ° κΈ°λ‘
β
4. μ¬μ©μ μνΈμμ© (ν΄λ¦, κ²μ λ±)
β POST /api/analytics/event
β customEvents ν
μ΄λΈμ μ μ₯
β
5. νμ΄μ§ μ΄ν μ
β 체λ₯ μκ° κ³μ°
β POST /api/analytics/page-view
β pageViews ν
μ΄λΈμ μ μ₯
// 1. μ΅κ·Ό 1μκ° λͺ¨λ μμ²
requests
| where timestamp > ago(1h)
| project timestamp, name, duration, resultCode
// 2. μΈλΆ API νΈμΆ μΆμ
dependencies
| where type == "HTTP"
| summarize count(), avg(duration) by target
// 3. μλ¬ λ‘κ·Έ μ‘°ν
traces
| where severityLevel >= 3
| project timestamp, message
// 4. νμ΄μ§λ³ λ°©λ¬Έ νμ
pageViews
| summarize view_count = count() by name
// 5. μ¬μ©μ μ΄λ²€νΈ λΆμ
customEvents
| where name == "search"
| extend query = tostring(customDimensions["query"])
| project timestamp, query
// 6. μ±λ₯ λ©νΈλ¦
customMetrics
| where name == "app.requests.duration"
| summarize avg(value) by bin(timestamp, 5m)
// 7. μμΈ μΆμ
exceptions
| summarize count() by type
| order by count_ descApplication Insights λͺ¨λν°λ§ λ° λΆμμ μν μ¬ν κ°μ΄λ:
- ν λ λ©νΈλ¦¬ ν μ΄λΈ κ°μ΄λ - 7κ°μ§ Application Insights ν μ΄λΈ μ€ν€λ§, νμ© μμ , KQL 쿼리
- μ¬μ©μ νλ λΆμ κ°μ΄λ - μ½νΈνΈ λΆμ, μ ν κΉλκΈ°, μ¬μ©μ μΈκ·Έλ¨ΌνΈ λΆμ
- Live Metrics κ°μ΄λ - μ€μκ° λͺ¨λν°λ§ μ€μ , μ¬μ©μ μ μ λ©νΈλ¦, νΈλ¬λΈμν
- λμ보λ μ€μ κ°μ΄λ - Azure Portal λμ보λ λ° Workbook ꡬμ±, KQL 쿼리 λͺ¨μ
- Cosmos DB λ€νΈμν¬ μ€μ - Cosmos DB λ°©νλ²½ μ€μ λ° Container App IP νμ© κ°μ΄λ
- App Insights KQL 쿼리 μ€μ - KQL 쿼리 μ€νμ μν νκ²½ μ€μ λ° κΆν κ΄λ¦¬
.env νμΌμ Container Registry μ λ³΄κ° μ€μ λμ΄ μλμ§ νμΈ:
# .env νμΌ νμΈ
cat .env | grep -E "CONTAINER_REGISTRY_NAME|RESOURCE_GROUP|LOCATION"
# μμ μΆλ ₯:
# CONTAINER_REGISTRY_NAME=crskappinsights
# RESOURCE_GROUP=rg-sk-appinsights
# LOCATION=koreacentral# Docker μ΄λ―Έμ§ λΉλ λ° ν
μ€νΈ (μλν μ€ν¬λ¦½νΈ)
./test-docker.sh
# λλ μλμΌλ‘
docker build -t etf-agent:local .
docker run -d --name etf-agent-test --env-file .env -p 8000:8000 etf-agent:local
# λ‘κ·Έ νμΈ
docker logs -f etf-agent-test
# μ€μ§ λ° μ κ±°
docker stop etf-agent-test
docker rm etf-agent-test# λͺ¨λ μλΉμ€ μμ
docker-compose up -d
# λ‘κ·Έ νμΈ
docker-compose logs -f
# μ€μ§
docker-compose downμλ λ°°ν¬ (μΆμ²):
# λ°°ν¬ μ€ν¬λ¦½νΈ μ€ν
./deploy-containerapp.sh
# νκ²½ λ³μ μν¬λ¦Ώ μ€μ
source .env
az containerapp secret set \
--name etf-agent-app \
--resource-group etf-agent-rg \
--secrets \
appinsights-connection-string="$APPLICATIONINSIGHTS_CONNECTION_STRING" \
cosmos-endpoint="$COSMOS_ENDPOINT" \
cosmos-key="$COSMOS_KEY" \
cosmos-database-name="$COSMOS_DATABASE_NAME" \
cosmos-container-name="$COSMOS_CONTAINER_NAME" \
openai-api-key="$OPENAI_API_KEY" \
alphavantage-api-key="$ALPHA_VANTAGE_API_KEY" \
finnhub-api-key="$FINNHUB_API_KEY"μμΈ κ°μ΄λ: Container App λ°°ν¬ κ°μ΄λ
μ½λλ₯Ό main λΈλμΉμ νΈμνλ©΄ μλμΌλ‘ λΉλ λ° λ°°ν¬λ©λλ€.
Repository β Settings β Secrets and variables β Actions
νμ Secrets:
AZURE_CREDENTIALS- Azure μλΉμ€ 주체 μΈμ¦ μ 보APPLICATIONINSIGHTS_CONNECTION_STRINGCOSMOS_ENDPOINT,COSMOS_KEY,COSMOS_DATABASE_NAME,COSMOS_CONTAINER_NAMEOPENAI_API_KEYALPHA_VANTAGE_API_KEY,FINNHUB_API_KEY(μ ν)
# Service Principal μμ± λ° JSON μΆλ ₯
az ad sp create-for-rbac \
--name "github-actions-etf-agent" \
--role contributor \
--scopes /subscriptions/{SUBSCRIPTION_ID}/resourceGroups/rg-sk-appinsights \
--sdk-auth
# μΆλ ₯λ μ 체 JSONμ AZURE_CREDENTIALS Secretμ μ μ₯# main λΈλμΉμ νΈμνλ©΄ μλ λ°°ν¬
git add .
git commit -m "feat: μλ‘μ΄ κΈ°λ₯ μΆκ°"
git push origin main
# GitHub Actionsμμ μλ μ€ν:
# 1. Docker μ΄λ―Έμ§ λΉλ
# 2. Azure Container Registry νΈμ
# 3. Container App λ°°ν¬GitHub Repository β Actions β "Deploy to Azure Container App" β Run workflow
μμΈ κ°μ΄λ: GitHub Actions μ€μ κ°μ΄λ
- CI (
ci.yml): Pull Request μ λ¦°νΈ, ν μ€νΈ, Docker λΉλ - CD (
deploy-containerapp.yml): main λΈλμΉ νΈμ μ μλ λ°°ν¬
# App URL κ°μ Έμ€κΈ°
APP_URL=$(az containerapp show \
--name etf-agent-app \
--resource-group etf-agent-rg \
--query properties.configuration.ingress.fqdn -o tsv)
echo "π App URL: https://$APP_URL"
echo "π Health: https://$APP_URL/health"
echo "π API Docs: https://$APP_URL/docs"
# Health check
curl https://$APP_URL/healthCtrl + / λ‘ μ§μΉ¨ νμΌ λͺ
μμ μΌλ‘ μ§μ
MIT License



