ftp目录日志通过flask展示简述

概述

业务中有时候权限控制的严格,日志目录是单独用1个目录存放,通过ftp形式查看;

此时想看日志就得先下载下来,而且想下载新日志时还得重新下载一遍,不是很方便;

所以在python环境下,简单用flask搭配socketio,通过文件内容游标配合ftp下载的增量内容在web页面展示出来;

版本
python:3.10.11
Flask-SocketIO:5.5.1

python代码

import ftplib
import time
import logging
import threading
from flask import Flask, render_template
from flask_socketio import SocketIO

# ——— 常量区 —————————————————————————————————————————
# FTP服务器配置
FTP_HOST = 'xxx.xxx.xxx.xxx'
FTP_USER = 'xxx'
FTP_PASS = 'xxx'
BASE_DIR      = '/'          # 扫描此目录下各子目录
LOG_FILENAME  = 'info.log'   # 要查找的日志文件名

POLL_INTERVAL = 1            # 日志增量读取间隔(秒)
SCAN_INTERVAL = 10           # 目录列表刷新间隔(秒)
# ——————————————————————————————————————————————————————

# 日志配置
logging.basicConfig(
    level    = logging.DEBUG,
    format   = '[%(asctime)s] %(levelname)s %(message)s',
    datefmt  = '%H:%M:%S'
)

app = Flask(__name__)
socketio = SocketIO(app, async_mode="threading", cors_allowed_origins="*")

# 定义各目录日志文件映射,key 为目录标识,将附加给前端以便区分日志来源
LOG_FILES = {
    'cloud-assist': '/cloud-assist/info.log',
    'cloud-product': '/cloud-product/info.log'
    # "join-purchase": "/join-purchase/info.log"
}

# 动态获取目录下的所有log_files,但是顺序读取很慢,要逐个输出到前端
def get_log_files(ftp, base_dir="/"):
    """
    在 FTP 服务器上遍历指定的基础目录,动态获取子目录中含有日志文件的映射。
    例如:如果 /logs/dir1 中存在 log.txt,则返回字典中包含
             {"dir1": "/logs/dir1/log.txt"}
    """
    log_files = {}

    try:
        # 保存当前目录,便于后续恢复
        current_dir = ftp.pwd()
        ftp.cwd(base_dir)
        # 列出基本目录下的所有条目
        items = ftp.nlst()
        for item in items:
            try:
                # 尝试切换到子目录,如果成功说明 item 是目录
                ftp.cwd(item)
                # 检查当前目录下是否有“log.txt”
                files = ftp.nlst()
                if "info.log" in files:
                    log_files[item] = f"{base_dir}/{item}/info.log"
                # 返回到基本目录
                ftp.cwd("..")
            except Exception:
                # 如果切换失败,则跳过该条目
                continue
        ftp.cwd(current_dir)
    except Exception as e:
        print("列出基础目录时出错:", e)
    return log_files


def ftp_login():
    """实时从FTP服务器下载日志新增内容,并通过SocketIO实时推送到前端。"""
    try:
        ftp = ftplib.FTP(FTP_HOST, timeout=10)
        ftp.login(FTP_USER, FTP_PASS)
        logging.info("FTP 登录成功")
        return ftp
    except Exception as e:
        logging.error("FTP 登录失败: %s", e)
        return None


def ftp_log_stream():
    """
    实时轮询 FTP 上动态获取的各日志文件,根据每个文件的偏移量读取新增部分,
    并对不完整断行进行拼接,最后通过 SocketIO 向前端发送数据(带上所在目录标识)。
    """
    # 对各日志文件保持偏移量与未完整行缓存
    offsets = {}
    leftovers = {}

    ftp = ftp_login()

    #log_files = get_log_files(ftp=ftp)

    while True:
        try:
            if ftp is None:
                ftp = ftp_login()

            # 针对每个检测到的目录进行处理
            for dir_key, remote_path in LOG_FILES.items():
                # 初始化状态(若是新检测到的日志文件)
                if dir_key not in offsets:
                    offsets[dir_key] = 0
                    leftovers[dir_key] = ''

                try:
                    current_size = ftp.size(remote_path)
                except Exception as e:
                    print(f"获取 {remote_path} 大小时出错:", e)
                    continue

                if current_size is None:
                    continue

                # 如果日志文件发生截断或轮换,则重置状态
                if current_size < offsets[dir_key]:
                    offsets[dir_key] = 0
                    leftovers[dir_key] = ''

                # 如果文件有新增内容,则仅断点续传读取新增部分
                if current_size > offsets[dir_key]:
                    new_data = bytearray()

                    def handle_binary(block):
                        new_data.extend(block)

                    ftp.retrbinary("RETR " + remote_path, callback=handle_binary, rest=offsets[dir_key])
                    offsets[dir_key] = current_size

                    # 解码数据,并拼接上一次未完整的断行
                    text_chunk = new_data.decode('utf-8', errors='ignore')
                    text_chunk = leftovers[dir_key] + text_chunk
                    lines = text_chunk.splitlines(keepends=True)

                    # 如果最后一行不完整,则保存到 leftovers,下次拼接
                    if lines and not lines[-1].endswith('\n'):
                        leftovers[dir_key] = lines.pop()
                    else:
                        leftovers[dir_key] = ''

                    # 逐行发送日志更新消息
                    for line in lines:
                        logging.debug(f"[EMIT] dir={dir_key} ln={line!r}")
                        # 发送的数据中带有所属目录标识,用于前端分区展示
                        socketio.emit('log_update', {'dir': dir_key, 'log': line})

            time.sleep(POLL_INTERVAL)
        except Exception as e:
            print("读取FTP日志时出现错误:", e)
            try:
                if ftp:
                    ftp.close()
            except Exception:
                pass
            ftp = None
            time.sleep(POLL_INTERVAL)

@app.route('/')
def index():
    return render_template('log_menu.html')

if __name__ == '__main__':
    threading.Thread(target=ftp_log_stream, daemon=True).start()
    #socketio.start_background_task(ftp_log_stream)

    socketio.run(app, host='127.0.0.1', port=5000, debug=True, allow_unsafe_werkzeug=True)

前端html

DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>多目录 FTP 日志实时展示title>
  
  <script src="https://cdn.socket.io/4.6.1/socket.io.min.js">script>
  <style>
    body {
      font-family: sans-serif;
      margin: 20px;
    }
    .tabs {
      display: flex;
      margin-bottom: 10px;
    }
    .tab {
      padding: 10px 20px;
      cursor: pointer;
      border: 1px solid #ccc;
      background: #f0f0f0;
      margin-right: 5px;
      transition: background-color 0.2s;
    }
    .tab:hover {
      background: #e0e0e0;
    }
    .tab.active {
      background: #fff;
      border-bottom: 2px solid #fff;
      font-weight: bold;
    }
    .log-container {
      border: 1px solid #ccc;
      height: 400px;
      overflow-y: auto;
      padding: 10px;
      background: #f9f9f9;
      font-family: monospace;
      display: none; /* 默认隐藏,只有当前激活的container显示 */
      white-space: pre-wrap;
    }
    .log-container.active {
      display: block;
    }
  style>
head>
<body>
  <h1>FTP 日志实时展示h1>

  
  <div class="tabs">
    <div class="tab active" data-dir="cloud-assist">cloud-assistdiv>
    <div class="tab" data-dir="cloud-product">cloud-productdiv>
    <div class="tab" data-dir="join-purchase">join-purchasediv>
  div>

  
  <div id="logs">
    <div class="log-container active" id="log-cloud-assist">div>
    <div class="log-container" id="log-cloud-product">div>
    <div class="log-container" id="log-join-purchase">div>
  div>

  <script>
    // 指定后端 WebSocket 地址及端口
    const socket = io("http://127.0.0.1:5000");

    // 监听 log_update 事件,将日志追加到对应的日志区域
    socket.on('log_update', data => {

      console.log("收到日志数据:", data);

      // data 应包含 data.dir(目录标识)和 data.log(日志内容)
      const dir = data.dir;
      const logLine = data.log;
      const container = document.getElementById('log-' + dir);
      if(container) {
        container.textContent += logLine;
        // 如果当前显示的是此日志区域,则自动滚动到底部
        if (container.classList.contains('active')) {
          container.scrollTop = container.scrollHeight;
        }
      }
    });


    socket.on("connect", () => {
      console.log("连接成功!");
    });

    socket.on("disconnect", () => {
      console.log("断开连接!");
    });

    socket.on('disconnect', () => console.log('⚠️ SOCKET DISCONNECTED'));
    socket.on('connect_error', err => console.error(' CONNECT ERROR', err));


    // 处理选项卡切换
    const tabs = document.querySelectorAll('.tab');
    tabs.forEach(tab => {
      tab.addEventListener('click', () => {
        // 移除所有 tab 和 log-container 的 active 类
        tabs.forEach(t => t.classList.remove('active'));
        document.querySelectorAll('.log-container').forEach(c => c.classList.remove('active'));

        // 激活当前 tab 以及对应的日志显示区域
        tab.classList.add('active');
        const dir = tab.getAttribute('data-dir');
        const container = document.getElementById('log-' + dir);
        if (container) {
          container.classList.add('active');
          // 若已滚动到底部,则新日志到来时自动滚动
          container.scrollTop = container.scrollHeight;
        }
      });
    });


  script>
body>
html>

你可能感兴趣的:(flask,python,后端)