reposync/src/api/Webhook.py

771 lines
31 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from fastapi import Request, HTTPException, WebSocket
from fastapi.responses import PlainTextResponse, HTMLResponse
from src.base.config import get_config
from src.router import WEBHOOK
from src.dao.issue_sync import SyncProjectDAO
from typing import Dict, Any, Optional
import requests
from datetime import datetime
import json
import hmac
import hashlib
from loguru import logger
from fastapi.responses import PlainTextResponse
import os
from src.utils.websocket_logger_fixed import manager, test_websocket_logging
# 设置日志配置
logger.add("logs/app.log", rotation="500 MB", retention="10 days")
def detect_webhook_source(data: dict, headers: dict) -> str:
"""检测webhook来源平台"""
# 通过请求头判断
if headers.get("X-Gitee-Token"):
return "Gitee"
elif headers.get("X-Hub-Signature-256"):
return "GitHub"
elif headers.get("X-Gitlink-Signature"):
return "GitLink"
# 通过数据结构判断
if "repository" in data:
if data["repository"].get("owner", {}).get("avatar_url"):
avatar_url = data["repository"]["owner"]["avatar_url"]
if "gitee" in avatar_url:
return "Gitee"
elif "github" in avatar_url:
return "GitHub"
elif "project" in data:
return "GitLink"
return "Unknown"
def get_webhook_event_info(data: dict, source: str) -> dict:
"""获取webhook事件详细信息"""
event_info = {
"source": source,
"event_type": "unknown",
"action": data.get("action", "unknown"),
"repository": "unknown",
"user": "unknown",
"title": "",
"number": 0
}
try:
if source in ["GitHub", "Gitee"]:
# GitHub/Gitee格式
repo = data.get("repository", {})
event_info["repository"] = repo.get("full_name", repo.get("name", "unknown"))
if "issue" in data:
event_info["event_type"] = "issue"
issue = data["issue"]
event_info["title"] = issue.get("title", "")
event_info["number"] = issue.get("number", 0)
event_info["user"] = issue.get("user", {}).get("login", "unknown")
elif "pull_request" in data:
event_info["event_type"] = "pull_request"
pr = data["pull_request"]
event_info["title"] = pr.get("title", "")
event_info["number"] = pr.get("number", 0)
event_info["user"] = pr.get("user", {}).get("login", "unknown")
elif "comment" in data:
event_info["event_type"] = "comment"
comment = data["comment"]
event_info["user"] = comment.get("user", {}).get("login", "unknown")
elif source == "GitLink":
# GitLink格式
project = data.get("project", {})
event_info["repository"] = project.get("name", "unknown")
if "issue" in data:
event_info["event_type"] = "issue"
issue = data["issue"]
event_info["title"] = issue.get("subject", "")
event_info["number"] = data.get("number", 0)
event_info["user"] = issue.get("author", {}).get("name", "unknown")
elif "pull_request" in data:
event_info["event_type"] = "pull_request"
pr = data["pull_request"]
event_info["title"] = pr.get("title", "")
event_info["number"] = pr.get("number", 0)
event_info["user"] = pr.get("user", {}).get("name", "unknown")
except Exception as e:
logger.error(f"❌ 解析webhook事件信息失败: {e}")
return event_info
async def find_project_by_repo_url(repo_url: str) -> Optional[int]:
"""
根据仓库URL查找对应的项目ID
"""
if not repo_url:
return None
try:
project_dao = SyncProjectDAO()
projects = await project_dao.list_all()
# 标准化输入的repo_url
normalized_input = repo_url.lower().rstrip('/').replace('.git', '')
for project in projects:
# 检查各个平台的仓库地址
for repo_field in [project.github, project.gitee, project.gitlink]:
if repo_field:
normalized_repo = repo_field.lower().rstrip('/').replace('.git', '')
if normalized_input == normalized_repo or normalized_input in normalized_repo:
logger.info(f"🎯 找到匹配项目: ID={project.id}, 名称={project.name}")
return project.id
logger.warning(f"⚠️ 未找到匹配的项目仓库URL: {repo_url}")
return None
except Exception as e:
logger.error(f"❌ 查找项目失败: {e}")
return None
async def get_project_id_from_webhook(data: dict) -> int:
"""
从webhook数据中提取项目ID
"""
repo_url = None
try:
# 从不同平台的webhook数据中提取仓库URL
if "repository" in data:
# GitHub/Gitee格式
if "html_url" in data["repository"]:
repo_url = data["repository"]["html_url"]
elif "url" in data["repository"]:
repo_url = data["repository"]["url"]
elif "clone_url" in data["repository"]:
repo_url = data["repository"]["clone_url"]
elif "project" in data:
# GitLink格式
project = data["project"]
if "html_url" in project:
repo_url = project["html_url"]
elif "git_url" in project:
repo_url = project["git_url"]
else:
# 如果没有直接的URL尝试构建URL
project_name = project.get("name")
project_owner = project.get("owner", {}).get("login") or project.get("owner", {}).get("name")
if project_name and project_owner:
repo_url = f"https://www.gitlink.org.cn/{project_owner}/{project_name}"
logger.info(f"🔍 从项目信息构建GitLink URL: {repo_url}")
if repo_url:
logger.info(f"🔍 尝试通过仓库URL匹配项目: {repo_url}")
project_id = await find_project_by_repo_url(repo_url)
if project_id:
logger.info(f"✅ 成功匹配到项目ID: {project_id}")
return project_id
else:
logger.warning(f"⚠️ 未找到匹配的项目: {repo_url}")
else:
logger.warning("⚠️ 无法从webhook数据中提取仓库URL")
# 如果找不到匹配的项目使用默认项目ID=1
logger.warning(f"🔄 使用默认项目ID=1这可能会导致同步失败")
return 1
except Exception as e:
logger.error(f"❌ 项目ID提取失败: {str(e)}")
return 1
async def get_sync_repo(project_id: int = 1):
"""获取同步仓库配置"""
config = await get_config(project_id)
return (
config.get("gitlink", {}).get("token"),
config.get("gitlink", {}).get("repo"),
config.get("gitlink", {}).get("owner"),
config.get("gitee", {}).get("token"),
config.get("gitee", {}).get("repo"),
config.get("gitee", {}).get("owner"),
config.get("github", {}).get("token"),
config.get("github", {}).get("repo"),
config.get("github", {}).get("owner")
)
async def verify_webhook_signature(request: Request, body: bytes, source: str, project_id: int = 1) -> None:
"""验证webhook签名"""
config = await get_config(project_id)
# 获取请求头中的签名
gitee_signature = request.headers.get("X-Gitee-Token")
github_signature = request.headers.get("X-Hub-Signature-256")
gitlink_signature = request.headers.get("X-Gitlink-Signature")
# 根据来源平台验证签名
if source == "Gitee":
gitee_webhook_secret = config.get("gitee", {}).get("webhook_secret")
if not gitee_webhook_secret:
logger.info("🔓 Gitee webhook签名验证跳过未配置secret")
return
if gitee_signature != gitee_webhook_secret:
logger.error("❌ Gitee webhook签名验证失败")
raise HTTPException(status_code=401, detail="Invalid Gitee webhook signature")
logger.info("✅ Gitee webhook签名验证成功")
elif source == "GitHub":
github_webhook_secret = config.get("github", {}).get("webhook_secret")
if not github_webhook_secret:
logger.info("🔓 GitHub webhook签名验证跳过未配置secret")
return
# GitHub使用HMAC-SHA256签名验证
expected_signature = hmac.new(
github_webhook_secret.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
expected_signature = f"sha256={expected_signature}"
if not hmac.compare_digest(github_signature, expected_signature):
logger.error("❌ GitHub webhook签名验证失败")
raise HTTPException(status_code=401, detail="Invalid GitHub webhook signature")
logger.info("✅ GitHub webhook签名验证成功")
elif source == "GitLink":
gitlink_webhook_secret = config.get("gitlink", {}).get("webhook_secret")
if not gitlink_webhook_secret:
logger.info("🔓 GitLink webhook签名验证跳过未配置secret")
return
if gitlink_signature != gitlink_webhook_secret:
logger.error("❌ GitLink webhook签名验证失败")
raise HTTPException(status_code=401, detail="Invalid GitLink webhook signature")
logger.info("✅ GitLink webhook签名验证成功")
else:
logger.warning(f"⚠️ 未知的webhook来源: {source}")
raise HTTPException(status_code=401, detail="Unknown webhook source")
async def sync_issue_to_gitee(data: dict, token: str, owner: str, repo: str) -> dict:
"""同步issue到Gitee"""
try:
issue = data["issue"]
title = issue["subject"]
body = issue.get("description", "")
# 在Gitee上创建issue
url = f'https://gitee.com/api/v5/repos/{owner}/issues'
payload = {
"access_token": token,
"repo": repo,
"title": title,
"body": body
}
headers = {
'Content-Type': 'application/json;charset=UTF-8'
}
logger.info(f"📤 向Gitee发送请求: POST {url}")
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
result = response.json()
return {
"issue_number": result.get("number"),
"url": result.get("html_url")
}
except Exception as e:
logger.error(f"❌ Gitee同步失败: {str(e)}")
raise
async def sync_issue_to_github(data: dict, token: str, owner: str, repo: str) -> dict:
"""同步issue到GitHub"""
try:
issue = data["issue"]
title = issue["subject"]
body = issue.get("description", "")
# 在GitHub上创建issue
url = f"https://api.github.com/repos/{owner}/{repo}/issues"
payload = {
"title": title,
"body": body,
"labels": ["sync issue"]
}
headers = {
'Authorization': f'Bearer {token}',
'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
'Content-Type': 'application/json'
}
logger.info(f"📤 向GitHub发送请求: POST {url}")
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
result = response.json()
return {
"issue_number": result.get("number"),
"url": result.get("html_url")
}
except Exception as e:
logger.error(f"❌ GitHub同步失败: {str(e)}")
raise
async def sync_issue_to_gitlink(data: dict, token: str, owner: str, repo: str) -> dict:
"""同步issue到GitLink"""
try:
# 从Gitee的webhook数据中提取issue信息
issue = data.get("issue", {})
title = issue.get("title", "")
body = issue.get("body", "")
# 构造GitLink API请求
url = f"https://www.gitlink.org.cn/api/v1/repos/{owner}/{repo}/issues"
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
payload = {
"subject": title,
"description": body,
"labels": ["sync issue"]
}
logger.info(f"📤 向GitLink发送请求: POST {url}")
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
result = response.json()
return {
"issue_number": result.get("number"),
"url": result.get("html_url")
}
except Exception as e:
logger.error(f"❌ GitLink同步失败: {str(e)}")
raise
@WEBHOOK.post("/issue")
async def issue_webhook(request: Request):
"""处理issue相关的webhook事件 - 自动识别项目"""
start_time = datetime.now()
try:
logger.info("🚀 =========================")
logger.info("🚀 收到Issue Webhook请求")
logger.info("🚀 =========================")
# 先读取请求体
body = await request.body()
# 解析JSON数据以获取项目信息
try:
data = json.loads(body.decode('utf-8'))
except json.JSONDecodeError as e:
logger.error(f"❌ JSON解析失败: {str(e)}")
return {"status": "error", "message": "Invalid JSON data"}
# 检测webhook来源和事件信息
source = detect_webhook_source(data, dict(request.headers))
event_info = get_webhook_event_info(data, source)
# 记录事件信息
logger.info(f"📡 来源平台: {event_info['source']}")
logger.info(f"📝 事件类型: {event_info['event_type']}")
logger.info(f"🎯 动作: {event_info['action']}")
logger.info(f"📁 仓库: {event_info['repository']}")
logger.info(f"👤 用户: {event_info['user']}")
logger.info(f"🏷️ 标题: {event_info['title']}")
logger.info(f"🔢 编号: #{event_info['number']}")
# 自动查找对应的项目ID
project_id = await get_project_id_from_webhook(data)
logger.info(f"🆔 使用项目ID: {project_id}")
if project_id == 1:
logger.warning("⚠️ 使用默认项目ID请检查项目配置")
# 验证签名
await verify_webhook_signature(request, body, event_info["source"], project_id)
# 获取仓库配置
row = await get_sync_repo(project_id)
gitlink_token, gitlink_repo, gitlink_owner = row[0:3]
gitee_token, gitee_repo, gitee_owner = row[3:6]
github_token, github_repo, github_owner = row[6:9]
# 验证配置完整性
if event_info["source"] == "GitLink":
if not all([gitlink_token, gitlink_repo, gitlink_owner]):
logger.error("❌ GitLink配置不完整")
return {"status": "error", "message": "GitLink configuration incomplete"}
elif event_info["source"] == "Gitee":
if not all([gitee_token, gitee_repo, gitee_owner]):
logger.error("❌ Gitee配置不完整")
return {"status": "error", "message": "Gitee configuration incomplete"}
elif event_info["source"] == "GitHub":
if not all([github_token, github_repo, github_owner]):
logger.error("❌ GitHub配置不完整")
return {"status": "error", "message": "GitHub configuration incomplete"}
# 处理issue事件
if event_info["event_type"] != "issue":
logger.warning(f"⚠️ 非issue事件: {event_info['event_type']}")
return {"status": "ignored", "message": "Not an issue event"}
if event_info["action"] not in ["opened", "edited", "closed", "reopened"]:
logger.info(f" 忽略的issue动作: {event_info['action']}")
return {"status": "ignored", "message": f"Ignored issue action: {event_info['action']}"}
# 同步处理
sync_results = []
# 从GitLink同步到其他平台
if event_info["source"] == "GitLink" and event_info["action"] == "opened":
logger.info("🔄 开始处理GitLink Issue事件")
logger.info("📝 创建Issue: #{} - {}", event_info["number"], event_info["title"])
logger.info("🔄 开始同步到Gitee和GitHub...")
# 同步到Gitee
if all([gitee_token, gitee_repo, gitee_owner]):
try:
logger.info("📤 正在向Gitee创建Issue...")
gitee_result = await sync_issue_to_gitee(data, gitee_token, gitee_owner, gitee_repo)
sync_results.append({"platform": "Gitee", "status": "success", "result": gitee_result})
logger.info("✅ Gitee Issue创建成功")
except Exception as e:
logger.error(f"❌ Gitee同步失败: {str(e)}")
sync_results.append({"platform": "Gitee", "status": "error", "error": str(e)})
# 同步到GitHub
if all([github_token, github_repo, github_owner]):
try:
logger.info("📤 正在向GitHub创建Issue...")
github_result = await sync_issue_to_github(data, github_token, github_owner, github_repo)
sync_results.append({"platform": "GitHub", "status": "success", "result": github_result})
logger.info("✅ GitHub Issue创建成功")
except Exception as e:
logger.error(f"❌ GitHub同步失败: {str(e)}")
sync_results.append({"platform": "GitHub", "status": "error", "error": str(e)})
# 从Gitee同步到其他平台
elif event_info["source"] == "Gitee" and event_info["action"] in ["open", "opened"]:
logger.info("🔄 开始处理Gitee Issue事件")
logger.info("📝 创建Issue: #{} - {}", event_info["number"], event_info["title"])
logger.info("🔄 开始同步到GitLink和GitHub...")
# 同步到GitLink
if all([gitlink_token, gitlink_repo, gitlink_owner]):
try:
logger.info("📤 正在向GitLink创建Issue...")
gitlink_result = await sync_issue_to_gitlink(data, gitlink_token, gitlink_owner, gitlink_repo)
sync_results.append({"platform": "GitLink", "status": "success", "result": gitlink_result})
logger.info("✅ GitLink Issue创建成功")
except Exception as e:
logger.error(f"❌ GitLink同步失败: {str(e)}")
sync_results.append({"platform": "GitLink", "status": "error", "error": str(e)})
# 同步到GitHub只有在GitHub配置存在时才同步
if all([github_token, github_repo, github_owner]):
try:
logger.info("📤 正在向GitHub创建Issue...")
github_result = await sync_issue_to_github(data, github_token, github_owner, github_repo)
sync_results.append({"platform": "GitHub", "status": "success", "result": github_result})
logger.info("✅ GitHub Issue创建成功")
except Exception as e:
logger.error(f"❌ GitHub同步失败: {str(e)}")
sync_results.append({"platform": "GitHub", "status": "error", "error": str(e)})
# 记录处理时间
end_time = datetime.now()
process_time = (end_time - start_time).total_seconds()
logger.info("🎉 =========================")
logger.info(f"🎉 处理完成! 耗时: {process_time:.2f}")
logger.info("🎉 =========================")
return {
"status": "success",
"message": "Webhook processed successfully",
"process_time": f"{process_time:.2f}s",
"sync_results": sync_results
}
except Exception as e:
logger.error(f"❌ Webhook处理失败: {str(e)}")
return {"status": "error", "message": f"Webhook processing failed: {str(e)}"}
@WEBHOOK.post("/pr")
async def pr_webhook(request: Request):
"""处理pull request相关的webhook事件 - 自动识别项目"""
start_time = datetime.now()
logger.info("🚀 =========================")
logger.info("🚀 收到PR Webhook请求")
logger.info("🚀 =========================")
# 先读取请求体
body = await request.body()
# 解析JSON数据以获取项目信息
data = json.loads(body.decode('utf-8'))
# 检测webhook来源和事件信息
source = detect_webhook_source(data, dict(request.headers))
event_info = get_webhook_event_info(data, source)
logger.info(f"📡 来源平台: {event_info['source']}")
logger.info(f"📝 事件类型: {event_info['event_type']}")
logger.info(f"🎯 动作: {event_info['action']}")
logger.info(f"📁 仓库: {event_info['repository']}")
logger.info(f"👤 用户: {event_info['user']}")
logger.info(f"🏷️ 标题: {event_info['title']}")
logger.info(f"🔢 编号: #{event_info['number']}")
# 自动查找对应的项目ID
project_id = await get_project_id_from_webhook(data)
logger.info(f"🆔 使用项目ID: {project_id}")
# 验证签名
await verify_webhook_signature(request, body, event_info["source"], project_id)
# 获取仓库配置
row = await get_sync_repo(project_id)
gitlink_token = row[0]
gitlink_repo = row[1]
gitlink_owner = row[2]
gitee_token = row[3]
gitee_repo = row[4]
gitee_owner = row[5]
github_token = row[6]
github_repo = row[7]
github_owner = row[8]
# 检测来源平台
if data["repository"]["owner"]["avatar_url"] and "gitee" in data["repository"]["owner"]["avatar_url"]:
flag = "gitee"
elif data["repository"]["owner"]["avatar_url"] and "github" in data["repository"]["owner"]["avatar_url"]:
flag = "github"
else:
flag = "gitlink"
logger.info(f"🔍 检测到平台: {flag}")
# 处理来自不同平台的PR事件 - 添加基础日志
try:
if flag == "gitee":
logger.info("🔄 开始处理Gitee PR事件")
# 现有的Gitee PR处理逻辑...
elif flag == "gitlink":
logger.info("🔄 开始处理GitLink PR事件")
# 现有的GitLink PR处理逻辑...
elif flag == "github":
logger.info("🔄 开始处理GitHub PR事件")
# 现有的GitHub PR处理逻辑...
except Exception as e:
logger.error(f"❌ PR处理失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# 在文件的最后,"未开通的服务"返回之前添加日志
logger.warning("⚠️ =========================")
logger.warning("⚠️ 未开通的服务或未支持的PR事件")
logger.warning(f"⚠️ 平台: {flag}")
logger.warning(f"⚠️ 动作: {data.get('action', 'unknown')}")
logger.warning("⚠️ =========================")
return {"message": "未开通的服务"}
@WEBHOOK.websocket("/admin/logs/ws")
async def websocket_logs(websocket: WebSocket):
"""WebSocket实时日志推送"""
await manager.connect(websocket)
try:
while True:
# 保持连接
await websocket.receive_text()
except:
manager.disconnect(websocket)
@WEBHOOK.get("/admin/logs/viewer", response_class=HTMLResponse)
async def logs_viewer():
"""日志查看器页面"""
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>实时日志查看器</title>
<meta charset="utf-8">
<style>
body {
font-family: 'Courier New', monospace;
margin: 0;
padding: 20px;
background-color: #1e1e1e;
color: #fff;
}
.header {
background-color: #333;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.controls {
display: flex;
gap: 10px;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary { background-color: #007bff; color: white; }
.btn-danger { background-color: #dc3545; color: white; }
.btn-warning { background-color: #ffc107; color: black; }
.log-container {
height: 70vh;
overflow-y: auto;
background-color: #000;
border: 1px solid #333;
padding: 10px;
border-radius: 5px;
font-size: 14px;
line-height: 1.4;
}
.log-entry {
margin-bottom: 5px;
padding: 3px 0;
word-wrap: break-word;
}
.log-timestamp { color: #888; }
.log-level { font-weight: bold; margin: 0 10px; }
.log-message { color: #fff; }
.level-INFO { color: #17a2b8; }
.level-ERROR { color: #dc3545; }
.level-WARNING { color: #ffc107; }
.level-DEBUG { color: #6c757d; }
.level-STDOUT { color: #28a745; }
.level-STDERR { color: #fd7e14; }
.status {
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
margin-left: 10px;
}
.status-connected { background-color: #28a745; }
.status-disconnected { background-color: #dc3545; }
.filter-container {
margin-bottom: 10px;
display: flex;
gap: 10px;
align-items: center;
}
select, input {
padding: 5px;
border: 1px solid #555;
border-radius: 3px;
background-color: #333;
color: #fff;
}
</style>
</head>
<body>
<div class="header">
<h1>🚀 FastAPI 实时日志查看器</h1>
<div class="controls">
<span id="status" class="status status-disconnected">未连接</span>
<button class="btn-primary" onclick="connect()">连接</button>
<button class="btn-danger" onclick="disconnect()">断开</button>
<button class="btn-warning" onclick="clearLogs()">清空</button>
</div>
</div>
<div class="filter-container">
<label>过滤级别:</label>
<select id="levelFilter" onchange="filterLogs()">
<option value="">全部</option>
<option value="INFO">INFO</option>
<option value="ERROR">ERROR</option>
<option value="WARNING">WARNING</option>
<option value="DEBUG">DEBUG</option>
<option value="STDOUT">STDOUT</option>
<option value="STDERR">STDERR</option>
</select>
<label>搜索:</label>
<input type="text" id="searchInput" placeholder="输入关键词搜索..." oninput="filterLogs()">
<label>自动滚动:</label>
<input type="checkbox" id="autoScroll" checked>
</div>
<div id="logs" class="log-container"></div>
<script>
let ws = null;
let allLogs = [];
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/webhook/admin/logs/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = function() {
document.getElementById('status').textContent = '已连接';
document.getElementById('status').className = 'status status-connected';
};
ws.onmessage = function(event) {
const logData = JSON.parse(event.data);
allLogs.push(logData);
// 限制日志数量,避免内存溢出
if (allLogs.length > 1000) {
allLogs = allLogs.slice(-800);
}
filterLogs();
};
ws.onclose = function() {
document.getElementById('status').textContent = '连接断开';
document.getElementById('status').className = 'status status-disconnected';
};
ws.onerror = function() {
document.getElementById('status').textContent = '连接错误';
document.getElementById('status').className = 'status status-disconnected';
};
}
function disconnect() {
if (ws) {
ws.close();
}
}
function clearLogs() {
allLogs = [];
document.getElementById('logs').innerHTML = '';
}
function filterLogs() {
const levelFilter = document.getElementById('levelFilter').value;
const searchText = document.getElementById('searchInput').value.toLowerCase();
const logsContainer = document.getElementById('logs');
let filteredLogs = allLogs;
if (levelFilter) {
filteredLogs = filteredLogs.filter(log => log.level === levelFilter);
}
if (searchText) {
filteredLogs = filteredLogs.filter(log =>
log.message.toLowerCase().includes(searchText)
);
}
logsContainer.innerHTML = filteredLogs.map(log => `