返回笔记首页

17.4 监控平台搭建

主题配置

一、技术实现方案

1.1 监控平台架构

plain
监控平台架构
  ├── 前端SDK层
  │   ├── 数据采集模块
  │   ├── 性能监控模块
  │   ├── 错误监控模块
  │   └── 埋点追踪模块
  │
  ├── 数据上报层
  │   ├── 上报策略
  │   ├── 数据压缩
  │   ├── 离线缓存
  │   └── 请求队列
  │
  ├── 服务端接收层
  │   ├── 数据接收API
  │   ├── 数据验证
  │   ├── 数据解析
  │   └── 数据清洗
  │
  ├── 数据存储层
  │   ├── MongoDB (原始数据)
  │   ├── Redis (实时计算)
  │   ├── ClickHouse (分析查询)
  │   └── ElasticSearch (日志检索)
  │
  ├── 数据分析层
  │   ├── 实时计算
  │   ├── 离线分析
  │   ├── 数据聚合
  │   └── 报表生成
  │
  └── 可视化展示层
      ├── 实时监控大屏
      ├── 性能分析报表
      ├── 错误统计图表
      └── 告警通知

1.2 技术栈

前端SDK

  • 构建工具: Rollup
  • 压缩库: pako (gzip)
  • 工具库: axios
服务端
  • Node.js + Express
  • 数据库: MongoDB, Redis, ClickHouse
  • 消息队列: RabbitMQ / Kafka
可视化
  • Vue 3 + ECharts
  • WebSocket (实时推送)

二、数据采集SDK (基于Rollup打包)

2.1 SDK项目结构

plain
monitor-sdk/
  ├── src/
  │   ├── core/
  │   │   ├── base.js           # 基础类
  │   │   ├── config.js         # 配置管理
  │   │   └── report.js         # 上报模块
  │   ├── modules/
  │   │   ├── performance.js    # 性能监控
  │   │   ├── error.js          # 错误监控
  │   │   ├── behavior.js       # 行为埋点
  │   │   └── exposure.js       # 曝光监控
  │   ├── utils/
  │   │   ├── compress.js       # 数据压缩
  │   │   ├── cache.js          # 缓存管理
  │   │   └── helpers.js        # 工具函数
  │   └── index.js              # 入口文件
  ├── rollup.config.js          # Rollup配置
  ├── package.json
  └── README.md

2.2 Rollup配置文件

rollup.config.js

javascript
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import terser from '@rollup/plugin-terser'
import json from '@rollup/plugin-json'

export default {
  input: 'src/index.js',

  output: [
    {
      file: 'dist/monitor-sdk.js',
      format: 'umd',
      name: 'MonitorSDK',
      sourcemap: true
    },
    {
      file: 'dist/monitor-sdk.min.js',
      format: 'umd',
      name: 'MonitorSDK',
      sourcemap: true,
      plugins: [terser()]
    },
    {
      file: 'dist/monitor-sdk.esm.js',
      format: 'es',
      sourcemap: true
    },
    {
      file: 'dist/monitor-sdk.cjs.js',
      format: 'cjs',
      sourcemap: true
    }
  ],

  plugins: [
    resolve(),
    commonjs(),
    json(),
    babel({
      babelHelpers: 'bundled',
      exclude: 'node_modules/**',
      presets: [
        ['@babel/preset-env', {
          targets: {
            browsers: ['> 1%', 'last 2 versions', 'not dead']
          }
        }]
      ]
    })
  ]
}

2.3 SDK核心代码

src/core/base.js

javascript
export class MonitorBase {
  constructor() {
    this.config = null
    this.modules = {}
  }

  // 初始化
  init(config) {
    this.config = {
      appId: config.appId,
      apiUrl: config.apiUrl,
      enablePerformance: config.enablePerformance !== false,
      enableError: config.enableError !== false,
      enableBehavior: config.enableBehavior !== false,
      enableExposure: config.enableExposure !== false,
      reportInterval: config.reportInterval || 5000,
      maxQueueSize: config.maxQueueSize || 10,
      sampleRate: config.sampleRate || 1,
      userId: config.userId,
      debug: config.debug || false
    }

    // 采样判断
    if (Math.random() > this.config.sampleRate) {
      console.log('Monitor SDK: Sampling skipped')
      return
    }

    this.initModules()
  }

  // 初始化模块
  initModules() {
    const modules = []

    if (this.config.enablePerformance) {
      modules.push('performance')
    }
    if (this.config.enableError) {
      modules.push('error')
    }
    if (this.config.enableBehavior) {
      modules.push('behavior')
    }
    if (this.config.enableExposure) {
      modules.push('exposure')
    }

    modules.forEach(moduleName => {
      this.loadModule(moduleName)
    })
  }

  // 加载模块
  loadModule(moduleName) {
    try {
      const Module = require(`../modules/${moduleName}`).default
      this.modules[moduleName] = new Module(this.config)
      this.modules[moduleName].init()
    } catch (error) {
      console.error(`Failed to load module: ${moduleName}`, error)
    }
  }

  // 获取模块
  getModule(moduleName) {
    return this.modules[moduleName]
  }

  // 销毁
  destroy() {
    Object.values(this.modules).forEach(module => {
      if (module.destroy) {
        module.destroy()
      }
    })
    this.modules = {}
  }
}
src/core/report.js
javascript
import { compress } from '../utils/compress'
import { CacheManager } from '../utils/cache'

export class ReportManager {
  constructor(config) {
    this.config = config
    this.queue = []
    this.cacheManager = new CacheManager()
    this.timer = null

    this.init()
  }

  init() {
    // 启动定时上报
    this.startScheduleReport()

    // 页面卸载时上报
    this.setupUnloadReport()

    // 恢复缓存数据
    this.recoverCache()
  }

  // 添加到队列
  add(data) {
    this.queue.push({
      ...data,
      appId: this.config.appId,
      userId: this.config.userId,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent
    })

    if (this.queue.length >= this.config.maxQueueSize) {
      this.report()
    }
  }

  // 上报数据
  async report() {
    if (this.queue.length === 0) return

    const data = [...this.queue]
    this.queue = []

    try {
      // 压缩数据
      const compressed = compress(data)

      // 发送数据
      await this.send(compressed)

      if (this.config.debug) {
        console.log('Report success:', data)
      }
    } catch (error) {
      console.error('Report failed:', error)

      // 保存到缓存
      this.cacheManager.save(data)
    }
  }

  // 发送数据
  async send(data) {
    // 优先使用Beacon API
    if (navigator.sendBeacon) {
      const blob = new Blob([data], { type: 'application/json' })
      const success = navigator.sendBeacon(this.config.apiUrl, blob)

      if (success) return
    }

    // 降级到fetch
    const response = await fetch(this.config.apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Encoding': 'gzip'
      },
      body: data,
      keepalive: true
    })

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }
  }

  // 定时上报
  startScheduleReport() {
    this.timer = setInterval(() => {
      this.report()
    }, this.config.reportInterval)
  }

  // 页面卸载上报
  setupUnloadReport() {
    window.addEventListener('beforeunload', () => {
      this.report()
    })

    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.report()
      }
    })
  }

  // 恢复缓存
  recoverCache() {
    const cached = this.cacheManager.load()
    if (cached && cached.length > 0) {
      this.queue.push(...cached)
      this.cacheManager.clear()
    }
  }

  // 销毁
  destroy() {
    if (this.timer) {
      clearInterval(this.timer)
    }
    this.report()
  }
}
src/utils/compress.js
javascript
import pako from 'pako'

// 压缩数据
export function compress(data) {
  try {
    const json = JSON.stringify(data)
    const compressed = pako.gzip(json)
    return compressed
  } catch (error) {
    console.error('Compress failed:', error)
    return JSON.stringify(data)
  }
}

// 解压数据
export function decompress(data) {
  try {
    const decompressed = pako.ungzip(data, { to: 'string' })
    return JSON.parse(decompressed)
  } catch (error) {
    console.error('Decompress failed:', error)
    return null
  }
}
src/utils/cache.js
javascript
export class CacheManager {
  constructor() {
    this.key = 'monitor_cache'
    this.maxSize = 100
  }

  // 保存
  save(data) {
    try {
      const cached = this.load() || []
      cached.push(...data)

      // 限制数量
      const limited = cached.slice(-this.maxSize)

      localStorage.setItem(this.key, JSON.stringify(limited))
    } catch (error) {
      console.error('Cache save failed:', error)
    }
  }

  // 加载
  load() {
    try {
      const cached = localStorage.getItem(this.key)
      return cached ? JSON.parse(cached) : []
    } catch (error) {
      console.error('Cache load failed:', error)
      return []
    }
  }

  // 清空
  clear() {
    try {
      localStorage.removeItem(this.key)
    } catch (error) {
      console.error('Cache clear failed:', error)
    }
  }
}
src/index.js
javascript
import { MonitorBase } from './core/base'
import { ReportManager } from './core/report'

class MonitorSDK extends MonitorBase {
  constructor() {
    super()
    this.reportManager = null
  }

  init(config) {
    super.init(config)

    // 初始化上报管理器
    this.reportManager = new ReportManager(this.config)
  }

  // 手动上报
  track(eventName, eventData) {
    this.reportManager.add({
      type: 'track',
      eventName,
      eventData
    })
  }

  // 设置用户ID
  setUserId(userId) {
    this.config.userId = userId
  }

  // 获取版本
  getVersion() {
    return '1.0.0'
  }
}

// 创建全局实例
const monitor = new MonitorSDK()

// 导出
export default monitor

// 支持script标签引入
if (typeof window !== 'undefined') {
  window.MonitorSDK = monitor
}
package.json
json
{
  "name": "monitor-sdk",
  "version": "1.0.0",
  "description": "前端监控SDK",
  "main": "dist/monitor-sdk.cjs.js",
  "module": "dist/monitor-sdk.esm.js",
  "browser": "dist/monitor-sdk.min.js",
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "test": "jest"
  },
  "keywords": ["monitor", "performance", "error", "tracking"],
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.23.0",
    "@babel/preset-env": "^7.23.0",
    "@rollup/plugin-babel": "^6.0.0",
    "@rollup/plugin-commonjs": "^25.0.0",
    "@rollup/plugin-json": "^6.0.0",
    "@rollup/plugin-node-resolve": "^15.0.0",
    "@rollup/plugin-terser": "^0.4.0",
    "rollup": "^4.0.0"
  },
  "dependencies": {
    "pako": "^2.1.0"
  }
}

三、数据上报策略

3.1 智能上报策略

smart-report-strategy.js

javascript
export class SmartReportStrategy {
  constructor(config) {
    this.config = config
    this.queue = {
      high: [],      // 高优先级:立即上报
      normal: [],    // 普通优先级:批量上报
      low: []        // 低优先级:延迟上报
    }

    this.init()
  }

  init() {
    // 监听网络状态
    this.monitorNetwork()

    // 根据优先级设置不同的上报策略
    this.setupReportStrategies()
  }

  // 添加数据
  add(data, priority = 'normal') {
    this.queue[priority].push({
      data,
      timestamp: Date.now()
    })

    this.checkReport(priority)
  }

  // 检查是否需要上报
  checkReport(priority) {
    const strategies = {
      high: () => {
        // 高优先级立即上报
        if (this.queue.high.length > 0) {
          this.report('high')
        }
      },
      normal: () => {
        // 普通优先级:10条或5秒
        if (this.queue.normal.length >= 10) {
          this.report('normal')
        }
      },
      low: () => {
        // 低优先级:50条或30秒
        if (this.queue.low.length >= 50) {
          this.report('low')
        }
      }
    }

    strategies[priority]?.()
  }

  // 设置上报策略
  setupReportStrategies() {
    // 普通优先级:5秒一次
    setInterval(() => {
      if (this.queue.normal.length > 0) {
        this.report('normal')
      }
    }, 5000)

    // 低优先级:30秒一次
    setInterval(() => {
      if (this.queue.low.length > 0) {
        this.report('low')
      }
    }, 30000)
  }

  // 上报数据
  async report(priority) {
    const items = [...this.queue[priority]]
    this.queue[priority] = []

    if (items.length === 0) return

    try {
      const data = items.map(item => item.data)
      await this.send(data, priority)
    } catch (error) {
      console.error('Report failed:', error)

      // 失败的数据降级处理
      if (priority === 'high') {
        // 高优先级失败,降级到普通
        this.queue.normal.push(...items)
      } else {
        // 其他优先级失败,缓存到localStorage
        this.saveToCache(items.map(item => item.data))
      }
    }
  }

  // 发送数据
  async send(data, priority) {
    const payload = {
      priority,
      data,
      timestamp: Date.now()
    }

    // 根据网络状况选择上报方式
    if (this.isSlowNetwork()) {
      // 慢速网络,只上报关键数据
      if (priority !== 'high') {
        throw new Error('Network too slow, skip non-high priority')
      }
    }

    const response = await fetch(this.config.apiUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
      keepalive: true
    })

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }
  }

  // 监控网络状态
  monitorNetwork() {
    if ('connection' in navigator) {
      this.networkInfo = navigator.connection

      navigator.connection.addEventListener('change', () => {
        console.log('Network changed:', {
          effectiveType: this.networkInfo.effectiveType,
          downlink: this.networkInfo.downlink,
          rtt: this.networkInfo.rtt
        })
      })
    }
  }

  // 判断是否为慢速网络
  isSlowNetwork() {
    if (!this.networkInfo) return false

    const effectiveType = this.networkInfo.effectiveType
    return effectiveType === 'slow-2g' || effectiveType === '2g'
  }

  // 保存到缓存
  saveToCache(data) {
    try {
      const cached = JSON.parse(localStorage.getItem('report_cache') || '[]')
      cached.push(...data)
      localStorage.setItem('report_cache', JSON.stringify(cached.slice(-100)))
    } catch (error) {
      console.error('Save to cache failed:', error)
    }
  }
}

四、数据清洗与存储

4.1 服务端数据处理

server/data-processor.js

javascript
class DataProcessor {
  // 数据验证
  validate(data) {
    if (!Array.isArray(data)) {
      return { valid: false, message: 'Data must be array' }
    }

    for (const item of data) {
      if (!item.appId) {
        return { valid: false, message: 'appId is required' }
      }
      if (!item.timestamp) {
        return { valid: false, message: 'timestamp is required' }
      }
    }

    return { valid: true }
  }

  // 数据清洗
  clean(data) {
    return data.map(item => {
      const cleaned = { ...item }

      // URL脱敏
      if (cleaned.url) {
        cleaned.url = this.desensitizeUrl(cleaned.url)
      }

      // 标准化时间
      cleaned.timestamp = new Date(cleaned.timestamp).toISOString()

      // 提取设备信息
      if (cleaned.userAgent) {
        cleaned.browser = this.extractBrowser(cleaned.userAgent)
        cleaned.os = this.extractOS(cleaned.userAgent)
        cleaned.device = this.extractDevice(cleaned.userAgent)
      }

      return cleaned
    })
  }

  // URL脱敏
  desensitizeUrl(url) {
    try {
      const urlObj = new URL(url)
      const sensitiveParams = ['token', 'password', 'key', 'secret']

      sensitiveParams.forEach(param => {
        urlObj.searchParams.delete(param)
      })

      return urlObj.toString()
    } catch (error) {
      return url
    }
  }

  // 提取浏览器
  extractBrowser(ua) {
    if (/Chrome/i.test(ua)) return 'Chrome'
    if (/Safari/i.test(ua)) return 'Safari'
    if (/Firefox/i.test(ua)) return 'Firefox'
    if (/Edge/i.test(ua)) return 'Edge'
    return 'Unknown'
  }

  // 提取操作系统
  extractOS(ua) {
    if (/Windows/i.test(ua)) return 'Windows'
    if (/Mac/i.test(ua)) return 'MacOS'
    if (/Linux/i.test(ua)) return 'Linux'
    if (/Android/i.test(ua)) return 'Android'
    if (/iOS/i.test(ua)) return 'iOS'
    return 'Unknown'
  }

  // 提取设备
  extractDevice(ua) {
    if (/Mobile/i.test(ua)) return 'Mobile'
    if (/Tablet/i.test(ua)) return 'Tablet'
    return 'Desktop'
  }

  // 数据分类
  categorize(data) {
    const categorized = {
      performance: [],
      error: [],
      behavior: [],
      exposure: []
    }

    data.forEach(item => {
      if (item.type === 'performance') {
        categorized.performance.push(item)
      } else if (item.type === 'error') {
        categorized.error.push(item)
      } else if (item.type === 'click' || item.type === 'track') {
        categorized.behavior.push(item)
      } else if (item.type === 'exposure') {
        categorized.exposure.push(item)
      }
    })

    return categorized
  }
}

module.exports = new DataProcessor()

五、可视化报表

5.1 监控大屏组件

MonitorDashboard.vue

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'

const statistics = ref({
  pv: 0,
  uv: 0,
  errorCount: 0,
  avgLoadTime: 0
})

const performanceChart = ref(null)
const errorChart = ref(null)

// 初始化图表
const initCharts = () => {
  initPerformanceChart()
  initErrorChart()
}

// 性能趋势图
const initPerformanceChart = () => {
  const chart = echarts.init(performanceChart.value)

  const option = {
    title: {
      text: '页面加载性能趋势',
      left: 'center'
    },
    tooltip: { trigger: 'axis' },
    legend: {
      data: ['FCP', 'LCP', 'TTFB'],
      bottom: 0
    },
    xAxis: {
      type: 'category',
      data: []
    },
    yAxis: {
      type: 'value',
      name: '时间 (ms)'
    },
    series: [
      {
        name: 'FCP',
        type: 'line',
        data: [],
        itemStyle: { color: '#409eff' }
      },
      {
        name: 'LCP',
        type: 'line',
        data: [],
        itemStyle: { color: '#67c23a' }
      },
      {
        name: 'TTFB',
        type: 'line',
        data: [],
        itemStyle: { color: '#e6a23c' }
      }
    ]
  }

  chart.setOption(option)
}

// 错误分布图
const initErrorChart = () => {
  const chart = echarts.init(errorChart.value)

  const option = {
    title: {
      text: '错误类型分布',
      left: 'center'
    },
    tooltip: {
      trigger: 'item'
    },
    series: [
      {
        type: 'pie',
        radius: '60%',
        data: []
      }
    ]
  }

  chart.setOption(option)
}

onMounted(() => {
  initCharts()
})
</script>

<template>
  <div class="monitor-dashboard">
    <div class="dashboard-header">
      <h1>监控大屏</h1>
    </div>

    <!-- 统计卡片 -->
    <div class="statistics-grid">
      <div class="stat-card">
        <div class="stat-label">今日PV</div>
        <div class="stat-value">{{ statistics.pv.toLocaleString() }}</div>
      </div>

      <div class="stat-card">
        <div class="stat-label">今日UV</div>
        <div class="stat-value">{{ statistics.uv.toLocaleString() }}</div>
      </div>

      <div class="stat-card">
        <div class="stat-label">错误数</div>
        <div class="stat-value">{{ statistics.errorCount }}</div>
      </div>

      <div class="stat-card">
        <div class="stat-label">平均加载</div>
        <div class="stat-value">{{ statistics.avgLoadTime }}ms</div>
      </div>
    </div>

    <!-- 图表 -->
    <div class="charts-grid">
      <div class="chart-card">
        <div ref="performanceChart" class="chart"></div>
      </div>

      <div class="chart-card">
        <div ref="errorChart" class="chart"></div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.monitor-dashboard {
  padding: 20px;
  background: #0a0e27;
  min-height: 100vh;
  color: white;
}

.dashboard-header h1 {
  font-size: 32px;
  text-align: center;
  margin-bottom: 30px;
}

.statistics-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
  margin-bottom: 30px;
}

.stat-card {
  padding: 30px;
  background: rgba(64, 158, 255, 0.1);
  border: 1px solid rgba(64, 158, 255, 0.2);
  border-radius: 12px;
  text-align: center;
}

.stat-label {
  font-size: 14px;
  color: rgba(255, 255, 255, 0.6);
  margin-bottom: 12px;
}

.stat-value {
  font-size: 36px;
  font-weight: 700;
}

.charts-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
}

.chart-card {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 12px;
  padding: 20px;
}

.chart {
  width: 100%;
  height: 400px;
}
</style>

六、告警机制

6.1 告警规则引擎

alert-engine.js

javascript
export class AlertEngine {
  constructor() {
    this.rules = []
    this.initDefaultRules()
  }

  // 初始化规则
  initDefaultRules() {
    this.addRule({
      id: 'high_error_rate',
      name: '错误率过高',
      condition: (data) => {
        return (data.errorCount / data.totalRequest) > 0.05
      },
      level: 'critical',
      message: (data) => `错误率${(data.errorCount / data.totalRequest * 100).toFixed(2)}%`,
      channels: ['dingtalk', 'email']
    })

    this.addRule({
      id: 'slow_page_load',
      name: '页面加载慢',
      condition: (data) => {
        return data.avgLoadTime > 3000
      },
      level: 'warning',
      message: (data) => `平均加载${data.avgLoadTime}ms`,
      channels: ['dingtalk']
    })
  }

  // 添加规则
  addRule(rule) {
    this.rules.push(rule)
  }

  // 检查告警
  async checkAlerts(data) {
    const alerts = []

    for (const rule of this.rules) {
      if (rule.condition(data)) {
        const alert = {
          id: Date.now(),
          ruleId: rule.id,
          ruleName: rule.name,
          level: rule.level,
          message: rule.message(data),
          timestamp: Date.now()
        }

        alerts.push(alert)
        await this.sendAlert(alert, rule.channels)
      }
    }

    return alerts
  }

  // 发送告警
  async sendAlert(alert, channels) {
    for (const channel of channels) {
      if (channel === 'dingtalk') {
        await this.sendToDingTalk(alert)
      } else if (channel === 'email') {
        await this.sendToEmail(alert)
      }
    }
  }

  // 发送钉钉
  async sendToDingTalk(alert) {
    const webhook = process.env.DINGTALK_WEBHOOK

    const content = {
      msgtype: 'markdown',
      markdown: {
        title: alert.ruleName,
        text: `### ${alert.ruleName}\n**级别**: ${alert.level}\n**消息**: ${alert.message}`
      }
    }

    await fetch(webhook, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(content)
    })
  }

  // 发送邮件
  async sendToEmail(alert) {
    console.log('Send email:', alert)
  }
}

七、简历描述模板

监控平台架构师 (2024.06 - 至今)

负责设计和搭建公司级前端监控平台,覆盖20+业务系统,日均处理监控数据100万+条。

核心职责

  • 基于Rollup构建监控SDK,实现全链路监控
  • 设计智能上报策略,数据丢失率<0.1%
  • 搭建数据处理系统,实现清洗和存储
  • 开发可视化大屏,实时展示监控数据
  • 建立告警系统,支持多渠道通知
技术实现
  • SDK采用模块化设计,压缩后15KB
  • 使用pako压缩,节省30%带宽
  • 数据存储采用MongoDB+Redis架构
  • WebSocket实时推送
  • 告警引擎支持规则配置
项目成果
  • SDK覆盖率100%
  • 日均处理100万数据
  • 错误响应时间从2小时到15分钟
  • 故障率下降60%

八、SOP标准回答

面试问题: 如何搭建监控平台?

标准回答

"监控平台分五层。第一层SDK用Rollup打包,支持tree-shaking。第二层上报有三种优先级策略。第三层服务端做数据清洗和验证。第四层数据存储用MongoDB原始数据、Redis实时计算。第五层可视化用ECharts展示。告警引擎支持规则配置和多渠道通知。"


九、部署配置

docker-compose.yml

yaml
version: '3.8'

services:
  monitor-api:
    build: ./server
    ports:
      - "3000:3000"
    environment:
      - MONGODB_URI=mongodb://mongo:27017/monitor
      - REDIS_URI=redis://redis:6379
    depends_on:
      - mongo
      - redis

  mongo:
    image: mongo:6.0
    volumes:
      - mongo-data:/data/db

  redis:
    image: redis:7.0
    volumes:
      - redis-data:/data

volumes:
  mongo-data:
  redis-data: