访问日志查询功能

支持分页、时间范围过滤和多种条件过滤,方便前端显示和管理API访问记录

python 复制代码
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, timedelta
from sqlalchemy import func, extract, and_, or_
import os

app = Flask(__name__)

# 数据库配置
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///api_monitor.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

class APIAccess(db.Model):
    __tablename__ = 'api_access'
    
    id = db.Column(db.Integer, primary_key=True, index=True)
    endpoint = db.Column(db.String(200), nullable=False)
    client_ip = db.Column(db.String(45))
    user_id = db.Column(db.Integer, nullable=True)
    response_status = db.Column(db.Integer)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    update_at = db.Column(db.String)

    def to_dict(self):
        return {
            'id': self.id,
            'endpoint': self.endpoint,
            'client_ip': self.client_ip,
            'user_id': self.user_id,
            'response_status': self.response_status,
            'created_at': self.created_at.isoformat() if self.created_at else None,
            'update_at': self.update_at
        }

# 创建数据库表
def init_db():
    with app.app_context():
        db.create_all()

# API 监控端点
@app.route('/api/monitor/stats', methods=['GET'])
def get_stats():
    """获取总体统计信息"""
    # 时间范围参数
    days = int(request.args.get('days', 7))
    start_date = datetime.utcnow() - timedelta(days=days)
    
    # 总访问量
    total_requests = APIAccess.query.filter(APIAccess.created_at >= start_date).count()
    
    # 成功请求 (2xx)
    success_requests = APIAccess.query.filter(
        and_(APIAccess.created_at >= start_date, 
             APIAccess.response_status >= 200, 
             APIAccess.response_status < 300)
    ).count()
    
    # 错误请求 (4xx, 5xx)
    error_requests = APIAccess.query.filter(
        and_(APIAccess.created_at >= start_date,
             or_(and_(APIAccess.response_status >= 400, APIAccess.response_status < 500),
                 and_(APIAccess.response_status >= 500, APIAccess.response_status < 600)))
    ).count()
    
    # 成功率
    success_rate = (success_requests / total_requests * 100) if total_requests > 0 else 0
    
    return jsonify({
        'total_requests': total_requests,
        'success_requests': success_requests,
        'error_requests': error_requests,
        'success_rate': round(success_rate, 2),
        'period_days': days
    })

@app.route('/api/monitor/endpoints', methods=['GET'])
def get_endpoint_stats():
    """按端点统计访问信息"""
    days = int(request.args.get('days', 7))
    start_date = datetime.utcnow() - timedelta(days=days)
    
    # 按端点分组统计
    endpoint_stats = db.session.query(
        APIAccess.endpoint,
        func.count(APIAccess.id).label('total_requests'),
        func.avg(APIAccess.response_status).label('avg_status'),
        func.min(APIAccess.created_at).label('first_access'),
        func.max(APIAccess.created_at).label('last_access')
    ).filter(APIAccess.created_at >= start_date)\
     .group_by(APIAccess.endpoint)\
     .order_by(func.count(APIAccess.id).desc())\
     .all()
    
    result = []
    for stat in endpoint_stats:
        result.append({
            'endpoint': stat.endpoint,
            'total_requests': stat.total_requests,
            'avg_status': round(stat.avg_status, 2) if stat.avg_status else None,
            'first_access': stat.first_access.isoformat() if stat.first_access else None,
            'last_access': stat.last_access.isoformat() if stat.last_access else None
        })
    
    return jsonify(result)

@app.route('/api/monitor/users', methods=['GET'])
def get_user_stats():
    """按用户统计访问信息"""
    days = int(request.args.get('days', 7))
    start_date = datetime.utcnow() - timedelta(days=days)
    
    user_stats = db.session.query(
        APIAccess.user_id,
        func.count(APIAccess.id).label('total_requests'),
        func.count(func.distinct(APIAccess.endpoint)).label('unique_endpoints')
    ).filter(
        and_(APIAccess.created_at >= start_date, APIAccess.user_id.isnot(None))
    ).group_by(APIAccess.user_id)\
     .order_by(func.count(APIAccess.id).desc())\
     .limit(20)\
     .all()
    
    result = []
    for stat in user_stats:
        result.append({
            'user_id': stat.user_id,
            'total_requests': stat.total_requests,
            'unique_endpoints': stat.unique_endpoints
        })
    
    return jsonify(result)

@app.route('/api/monitor/status-codes', methods=['GET'])
def get_status_code_stats():
    """按状态码统计"""
    days = int(request.args.get('days', 7))
    start_date = datetime.utcnow() - timedelta(days=days)
    
    status_stats = db.session.query(
        APIAccess.response_status,
        func.count(APIAccess.id).label('count')
    ).filter(APIAccess.created_at >= start_date)\
     .group_by(APIAccess.response_status)\
     .order_by(func.count(APIAccess.id).desc())\
     .all()
    
    result = []
    for stat in status_stats:
        result.append({
            'status_code': stat.response_status,
            'count': stat.count
        })
    
    return jsonify(result)

@app.route('/api/monitor/hourly', methods=['GET'])
def get_hourly_stats():
    """按小时统计访问量"""
    days = int(request.args.get('days', 1))
    start_date = datetime.utcnow() - timedelta(days=days)
    
    hourly_stats = db.session.query(
        extract('hour', APIAccess.created_at).label('hour'),
        func.count(APIAccess.id).label('requests')
    ).filter(APIAccess.created_at >= start_date)\
     .group_by(extract('hour', APIAccess.created_at))\
     .order_by(extract('hour', APIAccess.created_at))\
     .all()
    
    result = []
    for stat in hourly_stats:
        result.append({
            'hour': int(stat.hour),
            'requests': stat.requests
        })
    
    return jsonify(result)

@app.route('/api/monitor/logs', methods=['GET'])
def get_access_logs():
    """获取访问日志"""
    # 分页参数
    page = int(request.args.get('page', 1))
    per_page = int(request.args.get('per_page', 50))
    
    # 过滤参数
    endpoint = request.args.get('endpoint')
    user_id = request.args.get('user_id')
    status_code = request.args.get('status_code')
    days = int(request.args.get('days', 7))
    
    start_date = datetime.utcnow() - timedelta(days=days)
    
    query = APIAccess.query.filter(APIAccess.created_at >= start_date)
    
    if endpoint:
        query = query.filter(APIAccess.endpoint.like(f'%{endpoint}%'))
    if user_id:
        query = query.filter(APIAccess.user_id == int(user_id))
    if status_code:
        query = query.filter(APIAccess.response_status == int(status_code))
    
    # 按创建时间倒序
    logs = query.order_by(APIAccess.created_at.desc())\
                .paginate(page=page, per_page=per_page, error_out=False)
    
    result = {
        'logs': [log.to_dict() for log in logs.items],
        'pagination': {
            'page': logs.page,
            'per_page': logs.per_page,
            'total': logs.total,
            'pages': logs.pages,
            'has_next': logs.has_next,
            'has_prev': logs.has_prev
        }
    }
    
    return jsonify(result)

@app.route('/api/monitor/errors', methods=['GET'])
def get_error_analysis():
    """错误分析"""
    days = int(request.args.get('days', 7))
    start_date = datetime.utcnow() - timedelta(days=days)
    
    # 按状态码分组的错误统计
    error_stats = db.session.query(
        APIAccess.response_status,
        APIAccess.endpoint,
        func.count(APIAccess.id).label('count')
    ).filter(
        and_(APIAccess.created_at >= start_date,
             APIAccess.response_status >= 400)
    ).group_by(APIAccess.response_status, APIAccess.endpoint)\
     .order_by(func.count(APIAccess.id).desc())\
     .all()
    
    result = []
    for stat in error_stats:
        result.append({
            'status_code': stat.response_status,
            'endpoint': stat.endpoint,
            'count': stat.count
        })
    
    return jsonify(result)

if __name__ == '__main__':
    init_db()
    app.run(debug=True, host='0.0.0.0', port=5000)
python 复制代码
@app.route('/api/stats/overview', methods=['GET'])
def get_overview_stats():
    """
    获取API监控概览统计信息
    
    返回:
    - total_requests: 总请求数量
    - total_users: 总用户数量(去重)
    - today_requests: 今日请求数量
    - today_users: 今日用户数量(去重)
    """
    try:
        # 获取今天的日期范围
        today = date.today()
        today_start = datetime.combine(today, datetime.min.time())
        today_end = datetime.combine(today, datetime.max.time())
        
        # 1. 总请求数量
        total_requests = APIAccess.query.count()
        
        # 2. 总用户数量(去重,排除NULL值)
        total_users_query = db.session.query(
            func.count(distinct(APIAccess.user_id))
        ).filter(APIAccess.user_id.isnot(None))
        total_users = total_users_query.scalar() or 0
        
        # 3. 今日请求数量
        today_requests = APIAccess.query.filter(
            APIAccess.created_at >= today_start,
            APIAccess.created_at <= today_end
        ).count()
        
        # 4. 今日用户数量(去重,排除NULL值)
        today_users_query = db.session.query(
            func.count(distinct(APIAccess.user_id))
        ).filter(
            APIAccess.user_id.isnot(None),
            APIAccess.created_at >= today_start,
            APIAccess.created_at <= today_end
        )
        today_users = today_users_query.scalar() or 0
        
        # 返回统计结果
        result = {
            'total_requests': total_requests,
            'total_users': total_users,
            'today_requests': today_requests,
            'today_users': today_users,
            'date': today.isoformat(),
            'timestamp': datetime.utcnow().isoformat()
        }
        
        return jsonify(result)
        
    except Exception as e:
        return jsonify({
            'error': '获取统计信息失败',
            'message': str(e)
        }), 500
python 复制代码
@app.route('/api/stats/chart', methods=['GET'])
def get_chart_data():
    """
    获取访问量折线图数据
    
    查询参数:
    - start_date: 开始日期 (格式: YYYY-MM-DD,可选)
    - end_date: 结束日期 (格式: YYYY-MM-DD,可选)
    - endpoint: 端点路径过滤(可选)
    - user_id: 用户ID过滤(可选)
    
    如果不提供日期,默认最近7天
    """
    try:
        # 获取日期参数
        start_date_str = request.args.get('start_date')
        end_date_str = request.args.get('end_date')
        endpoint = request.args.get('endpoint')
        user_id = request.args.get('user_id')
        
        # 处理日期范围
        if start_date_str and end_date_str:
            try:
                start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
                end_date = datetime.strptime(end_date_str + ' 23:59:59', '%Y-%m-%d %H:%M:%S')
            except ValueError:
                return jsonify({'error': '日期格式错误,请使用 YYYY-MM-DD 格式'}), 400
        else:
            # 默认最近7天
            end_date = datetime.utcnow()
            start_date = end_date - timedelta(days=7)
        
        # 构建查询条件
        conditions = [APIAccess.created_at >= start_date, APIAccess.created_at <= end_date]
        
        if endpoint:
            conditions.append(APIAccess.endpoint.like(f'%{endpoint}%'))
        
        if user_id:
            try:
                conditions.append(APIAccess.user_id == int(user_id))
            except ValueError:
                return jsonify({'error': '无效的用户ID'}), 400
        
        # 计算天数差,决定聚合方式
        days_diff = (end_date - start_date).days
        
        if days_diff <= 1:
            # 1天内按小时显示
            chart_data = get_hourly_data(conditions, start_date, end_date)
        else:
            # 多天按日期显示
            chart_data = get_daily_data(conditions, start_date, end_date)
        
        # 计算总访问量
        total_requests = db.session.query(func.count(APIAccess.id)).filter(*conditions).scalar()
        
        result = {
            'labels': chart_data['labels'],
            'data': chart_data['data'],
            'period': chart_data['period'],
            'total': total_requests,
            'date_range': {
                'start': start_date.strftime('%Y-%m-%d'),
                'end': end_date.strftime('%Y-%m-%d')
            },
            'filters': {
                'endpoint': endpoint,
                'user_id': user_id
            }
        }
        
        return jsonify(result)
        
    except Exception as e:
        return jsonify({
            'error': '获取图表数据失败',
            'message': str(e)
        }), 500

def get_daily_data(conditions, start_date, end_date):
    """按天聚合数据"""
    daily_stats = db.session.query(
        func.date(APIAccess.created_at).label('date'),
        func.count(APIAccess.id).label('count')
    ).filter(*conditions)\
     .group_by(func.date(APIAccess.created_at))\
     .order_by(func.date(APIAccess.created_at))\
     .all()
    
    # 转换为字典
    stats_dict = {stat.date.isoformat(): stat.count for stat in daily_stats}
    
    # 生成完整的时间序列
    labels = []
    data = []
    current = start_date.date()
    end = end_date.date()
    
    while current <= end:
        date_str = current.isoformat()
        labels.append(current.strftime('%m-%d'))
        data.append(stats_dict.get(date_str, 0))
        current += timedelta(days=1)
    
    return {
        'labels': labels,
        'data': data,
        'period': 'daily'
    }

def get_hourly_data(conditions, start_date, end_date):
    """按小时聚合数据"""
    hourly_stats = db.session.query(
        extract('hour', APIAccess.created_at).label('hour'),
        func.count(APIAccess.id).label('count')
    ).filter(*conditions)\
     .group_by(extract('hour', APIAccess.created_at))\
     .order_by(extract('hour', APIAccess.created_at))\
     .all()
    
    # 转换为字典
    stats_dict = {int(stat.hour): stat.count for stat in hourly_stats}
    
    # 生成24小时的数据
    labels = []
    data = []
    
    for hour in range(24):
        labels.append(f"{hour:02d}:00")
        data.append(stats_dict.get(hour, 0))
    
    return {
        'labels': labels,
        'data': data,
        'period': 'hourly'
    }
python 复制代码
from flask import Flask, jsonify, request
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy import create_engine, func, extract, and_, or_
from datetime import datetime, timedelta
import os
from contextlib import contextmanager

# 数据库配置
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///api_monitor.db')

# 创建引擎
engine = create_engine(DATABASE_URL, echo=False)

# 创建 SessionLocal
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 导入模型(需要先定义APIAccess模型)
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

class APIAccess(Base):
    __tablename__ = 'api_access'
    
    id = Base.Column(Base.Integer, primary_key=True, index=True)
    endpoint = Base.Column(Base.String(200), nullable=False)
    client_ip = Base.Column(Base.String(45))
    user_id = Base.Column(Base.Integer, nullable=True)
    response_status = Base.Column(Base.Integer)
    created_at = Base.Column(Base.DateTime, default=datetime.utcnow)
    update_at = Base.Column(Base.DateTime, default=datetime.utcnow)

app = Flask(__name__)

@contextmanager
def get_db():
    """数据库会话上下文管理器"""
    session = SessionLocal()
    try:
        yield session
    finally:
        session.close()

@app.route('/api/stats/overview', methods=['GET'])
def get_overview_stats():
    """获取概览统计信息"""
    try:
        with get_db() as session:
            # 获取今天的日期范围
            today = datetime.utcnow().date()
            today_start = datetime.combine(today, datetime.min.time())
            today_end = datetime.combine(today, datetime.max.time())
            
            # 总请求数量
            total_requests = session.query(func.count(APIAccess.id)).scalar()
            
            # 总用户数量(去重)
            total_users = session.query(func.count(func.distinct(APIAccess.user_id)))\
                               .filter(APIAccess.user_id.isnot(None)).scalar() or 0
            
            # 今日请求数量
            today_requests = session.query(func.count(APIAccess.id))\
                                  .filter(APIAccess.created_at >= today_start,
                                          APIAccess.created_at <= today_end).scalar()
            
            # 今日用户数量(去重)
            today_users = session.query(func.count(func.distinct(APIAccess.user_id)))\
                               .filter(APIAccess.user_id.isnot(None),
                                       APIAccess.created_at >= today_start,
                                       APIAccess.created_at <= today_end).scalar() or 0
            
            result = {
                'total_requests': total_requests,
                'total_users': total_users,
                'today_requests': today_requests,
                'today_users': today_users,
                'date': today.isoformat(),
                'timestamp': datetime.utcnow().isoformat()
            }
            
            return jsonify(result)
            
    except Exception as e:
        return jsonify({
            'error': '获取统计信息失败',
            'message': str(e)
        }), 500

@app.route('/api/stats/chart', methods=['GET'])
def get_chart_data():
    """获取访问量折线图数据"""
    try:
        # 获取参数
        start_date_str = request.args.get('start_date')
        end_date_str = request.args.get('end_date')
        endpoint = request.args.get('endpoint')
        user_id = request.args.get('user_id')
        
        # 处理日期范围
        if start_date_str and end_date_str:
            try:
                start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
                end_date = datetime.strptime(end_date_str + ' 23:59:59', '%Y-%m-%d %H:%M:%S')
            except ValueError:
                return jsonify({'error': '日期格式错误,请使用 YYYY-MM-DD 格式'}), 400
        else:
            # 默认最近7天
            end_date = datetime.utcnow()
            start_date = end_date - timedelta(days=7)
        
        with get_db() as session:
            # 构建查询条件
            conditions = [APIAccess.created_at >= start_date, APIAccess.created_at <= end_date]
            
            if endpoint:
                conditions.append(APIAccess.endpoint.like(f'%{endpoint}%'))
            
            if user_id:
                try:
                    conditions.append(APIAccess.user_id == int(user_id))
                except ValueError:
                    return jsonify({'error': '无效的用户ID'}), 400
            
            # 计算天数差,决定聚合方式
            days_diff = (end_date - start_date).days
            
            if days_diff <= 1:
                # 1天内按小时显示
                chart_data = get_hourly_data_session(session, conditions, start_date, end_date)
            else:
                # 多天按日期显示
                chart_data = get_daily_data_session(session, conditions, start_date, end_date)
            
            # 计算总访问量
            total_requests = session.query(func.count(APIAccess.id)).filter(*conditions).scalar()
            
            result = {
                'labels': chart_data['labels'],
                'data': chart_data['data'],
                'period': chart_data['period'],
                'total': total_requests,
                'date_range': {
                    'start': start_date.strftime('%Y-%m-%d'),
                    'end': end_date.strftime('%Y-%m-%d')
                },
                'filters': {
                    'endpoint': endpoint,
                    'user_id': user_id
                }
            }
            
            return jsonify(result)
            
    except Exception as e:
        return jsonify({
            'error': '获取图表数据失败',
            'message': str(e)
        }), 500

def get_daily_data_session(session, conditions, start_date, end_date):
    """按天聚合数据(使用session参数)"""
    daily_stats = session.query(
        func.date(APIAccess.created_at).label('date'),
        func.count(APIAccess.id).label('count')
    ).filter(*conditions)\
     .group_by(func.date(APIAccess.created_at))\
     .order_by(func.date(APIAccess.created_at))\
     .all()
    
    # 转换为字典
    stats_dict = {stat.date.isoformat(): stat.count for stat in daily_stats}
    
    # 生成完整的时间序列
    labels = []
    data = []
    current = start_date.date()
    end = end_date.date()
    
    while current <= end:
        date_str = current.isoformat()
        labels.append(current.strftime('%m-%d'))
        data.append(stats_dict.get(date_str, 0))
        current += timedelta(days=1)
    
    return {
        'labels': labels,
        'data': data,
        'period': 'daily'
    }

def get_hourly_data_session(session, conditions, start_date, end_date):
    """按小时聚合数据(使用session参数)"""
    hourly_stats = session.query(
        extract('hour', APIAccess.created_at).label('hour'),
        func.count(APIAccess.id).label('count')
    ).filter(*conditions)\
     .group_by(extract('hour', APIAccess.created_at))\
     .order_by(extract('hour', APIAccess.created_at))\
     .all()
    
    # 转换为字典
    stats_dict = {int(stat.hour): stat.count for stat in hourly_stats}
    
    # 生成24小时的数据
    labels = []
    data = []
    
    for hour in range(24):
        labels.append(f"{hour:02d}:00")
        data.append(stats_dict.get(hour, 0))
    
    return {
        'labels': labels,
        'data': data,
        'period': 'hourly'
    }

@app.route('/api/logs', methods=['GET'])
def get_access_logs():
    """获取访问日志信息"""
    try:
        # 获取分页参数
        page = int(request.args.get('page', 1))
        per_page = int(request.args.get('per_page', 50))
        
        # 获取过滤参数
        start_date_str = request.args.get('start_date')
        end_date_str = request.args.get('end_date')
        endpoint = request.args.get('endpoint')
        user_id = request.args.get('user_id')
        status_code = request.args.get('status_code')
        
        with get_db() as session:
            # 构建查询
            query = session.query(APIAccess)
            
            # 时间范围过滤
            if start_date_str and end_date_str:
                try:
                    start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
                    end_date = datetime.strptime(end_date_str + ' 23:59:59', '%Y-%m-%d %H:%M:%S')
                    query = query.filter(APIAccess.created_at >= start_date, APIAccess.created_at <= end_date)
                except ValueError:
                    return jsonify({'error': '日期格式错误,请使用 YYYY-MM-DD 格式'}), 400
            else:
                # 默认最近7天
                end_date = datetime.utcnow()
                start_date = end_date - timedelta(days=7)
                query = query.filter(APIAccess.created_at >= start_date, APIAccess.created_at <= end_date)
            
            # 其他过滤条件
            if endpoint:
                query = query.filter(APIAccess.endpoint.like(f'%{endpoint}%'))
            
            if user_id:
                try:
                    query = query.filter(APIAccess.user_id == int(user_id))
                except ValueError:
                    return jsonify({'error': '无效的用户ID'}), 400
            
            if status_code:
                try:
                    query = query.filter(APIAccess.response_status == int(status_code))
                except ValueError:
                    return jsonify({'error': '无效的状态码'}), 400
            
            # 获取总数
            total = query.count()
            
            # 计算分页
            offset = (page - 1) * per_page
            
            # 按时间倒序,分页查询
            logs = query.order_by(APIAccess.created_at.desc()).offset(offset).limit(per_page).all()
            
            # 格式化日志数据
            logs_data = []
            for log in logs:
                logs_data.append({
                    'id': log.id,
                    'time': log.created_at.isoformat() if log.created_at else None,
                    'endpoint': log.endpoint,
                    'user': log.user_id,
                    'status_code': log.response_status,
                    'client_ip': log.client_ip
                })
            
            # 计算总页数
            pages = (total + per_page - 1) // per_page
            
            # 返回结果
            result = {
                'logs': logs_data,
                'pagination': {
                    'page': page,
                    'per_page': per_page,
                    'total': total,
                    'pages': pages,
                    'has_next': page < pages,
                    'has_prev': page > 1
                },
                'filters': {
                    'start_date': start_date_str,
                    'end_date': end_date_str,
                    'endpoint': endpoint,
                    'user_id': user_id,
                    'status_code': status_code
                }
            }
            
            return jsonify(result)
            
    except Exception as e:
        return jsonify({
            'error': '获取访问日志失败',
            'message': str(e)
        }), 500

if __name__ == '__main__':
    # 创建表
    Base.metadata.create_all(bind=engine)
    
    app.run(debug=True, host='0.0.0.0', port=5000)
相关推荐
星光一影2 小时前
Java版社交系统/聊天系统/im/即时通信/社交通讯
java·spring boot·mysql·交友
R.lin2 小时前
对接物联网使用netty通信与MQTT之间的区别
java·物联网
李昊哲小课2 小时前
Ubuntu 24.04 安装开源WebRTC信令服务器
服务器·ubuntu·mediasoup·janus·信令服务器
AI_56782 小时前
CI/CD自动化部署革命:“三分钟流水线“背后的工程实践
java·开发语言·人工智能·ai·neo4j
这个人需要休息2 小时前
TCP/IP 协议栈
服务器·网络·网络协议·tcp/ip
kkce2 小时前
快快测(KKCE)TCping 检测全面升级:IPv6 深度覆盖 + 多维度可视化,重构网络性能监测新体验
服务器·网络·重构
dragoooon342 小时前
[Linux——Lesson23.线程概念与控制:线程基础]
java·开发语言·jvm
浪漫血液&2 小时前
进程调度的基本过程
服务器·进程调度
YJlio2 小时前
VMMap 学习笔记(8.2):启动 VMMap、选择目标进程、权限要求与首次快照流程
服务器·笔记·学习