
1. “Hindsight”不是工具名而是开发者对技术复盘的集体隐喻最近在多个技术社区和开源项目讨论区里“hindsight”这个词高频出现但它从不作为正式软件包名、CLI命令或GitHub仓库主名称被注册或发布。它没有官方文档没有npm registry页面也不在PyPI上存在安装记录——可偏偏工程师们在调试失败的Docker容器、排查OpenAI API调用超时、修复npm peer dependency警告时脱口而出的第一句话常是“这时候真需要一点hindsight。”这个词本身是英语中一个具象又微妙的复合词hind后sight视野直译为“向后看的视力”中文语境里最贴切的对应是“事后诸葛亮”式的清醒但技术圈赋予了它更沉实的分量它指代一种在系统已运行、错误已发生、日志已堆积之后仍能穿透噪声、定位根因、还原完整因果链的技术能力。它不是预测而是回溯不依赖监控告警的提前触发而依赖对数据流、状态变迁、依赖拓扑的深度理解与结构化重建。为什么这个概念突然密集浮现观察热搜词组合就能看出端倪pythonnpmdockeropenai构成了一条典型的现代AI工程交付链路——Python写逻辑、npm管理前端/CLI工具链、Docker封装环境、OpenAI提供模型能力。这条链路表面平滑实则布满“隐性耦合断点”Python脚本调用OpenAI SDK时requests超时设置被Docker容器内DNS解析延迟掩盖npm install时的eresolve overriding peer dependency警告在本地开发机上无感却让Docker构建阶段的node_modules体积暴增300%最终导致Lambda冷启动超时docker desktop failed to start because virtualization support not detected错误提示看似明确但真实原因可能是Windows Hyper-V与WSL2驱动冲突而该冲突只在启用OpenAI本地代理服务如cline后才被触发。这些都不是单一工具的Bug而是多层抽象栈叠加后产生的涌现性故障。此时“hindsight”就不再是修辞而是刚需——你必须像法医一样解剖整个执行现场从Python进程的内存堆栈、npm解析器的依赖图快照、Docker守护进程的cgroup限制日志到OpenAI请求头中被自动注入的X-Forwarded-For字段。我去年重构一个量化交易信号服务时连续三天卡在“策略回测结果本地一致Docker部署后偏差17%”的问题上。最后发现根源是Python 3.9在Alpine Linux镜像中默认启用-O优化模式丢弃assert而某处关键的价格校验逻辑恰巧写在assert里。这个细节在任何官方文档、Dockerfile注释、甚至CI日志里都毫无痕迹——它只存在于python -c import sys; print(sys.flags)的输出里。那一刻我深刻体会到hindsight不是靠工具给的是靠你亲手把每一层抽象掀开、把每一份默认值抠出来、把每一个“理所当然”打上问号换来的。所以本文不教你安装Docker Desktop不帮你配npm国内源也不演示如何获取OpenAI API Key。我们要做的是构建一套可复用的hindsight方法论当你的Python脚本在Docker里静默退出、npm run build卡在95%、OpenAI响应突然返回429却查不到限流日志时你能立刻启动的、结构化的回溯引擎。2. 回溯三原色时间线、状态快照与依赖图谱的交叉验证真正的hindsight不是靠灵光一现而是三组独立证据链的严丝合缝。我把它们称为“回溯三原色”时间线Timeline、状态快照State Snapshot和依赖图谱Dependency Graph。任何单一维度的分析都可能导向错误归因——就像只看监控曲线会误判为CPU瓶颈而实际是磁盘I/O锁死只查日志会以为是代码异常而真相是环境变量未注入导致配置为空。2.1 时间线不是日志时间戳而是事件因果链新手常犯的错误是直接grep日志里的ERROR关键字然后顺着时间戳往下读。这极不可靠因为多个进程的日志时间戳不同步Docker容器内NTP未同步、宿主机与容器时区不一致关键错误往往发生在“安静期”——比如OpenAI请求超时前30秒Docker网络驱动已开始丢包但日志里只有levelinfo msgreceived packet这类无害记录真正的触发事件可能早于错误日志数分钟npm install的peer dependency警告出现在构建开始时但直到npm run build执行到webpack解析阶段才暴露为模块缺失。正确做法是构建跨进程事件因果链。以一个典型故障为例Python Flask服务在Docker中启动后首次调用OpenAI API返回ConnectionResetError重试3次后降级为本地LLM。但docker logs -f只显示[2024-05-20 14:22:31] ERROR in app: OpenAI request failed无堆栈。此时应立即执行三步操作锚定绝对时间点在宿主机执行date -u %s.%N记录当前UTC纳秒时间戳如1716214951.123456789采集全栈时间线索# 宿主机网络事件需root sudo tcpdump -i any -w /tmp/hindsight.pcap port 443 and host api.openai.com -G 60 -W 1 # Docker守护进程日志精确到微秒 journalctl -u docker --since 2024-05-20 14:22:00 --until 2024-05-20 14:23:00 -o cat | grep -E (start|stop|network|cni) # 容器内Python进程系统调用需提前安装strace docker exec -it my-app strace -p $(pgrep -f gunicorn.*app:app) -e traceconnect,sendto,recvfrom -s 1024 -T -ttt 21 | head -n 50对齐时间轴将三组输出按时间戳排序注意journalctl默认用本地时区tcpdump用UTCstrace用容器时钟你会发现1716214951.123strace捕获到connect(3, {sa_familyAF_INET, sin_porthtons(443), ...}, 16) 0连接成功1716214951.456tcpdump显示14:22:31.456789 IP 172.17.0.2.54321 104.18.25.12.443: Flags [S]SYN包发出1716214951.789journalctl出现levelwarning msgfailed to setup network for container ... timeout waiting for CNI result真相浮出水面不是OpenAI服务问题而是Docker网络插件在容器启动后未能及时分配IP导致Python进程虽建立TCP连接但底层路由未就绪后续数据包被内核丢弃。这个结论无法从任何单一时序日志得出必须三线并行比对。提示时间线对齐的关键是统一时间基准。永远用UTC纳秒戳date -u %s.%N避免date %Y-%m-%d %H:%M:%S这种精度不足且有时区陷阱的格式。Docker容器内若需高精度时间务必在Dockerfile中添加RUN apk add --no-cache tzdata cp /usr/share/zoneinfo/UTC /etc/localtime。2.2 状态快照冻结运行时的“数字切片”时间线告诉你“何时发生”状态快照则回答“发生时系统长什么样”。它不是简单的ps aux或df -h而是对内存、文件系统、网络栈、进程上下文四个维度的原子级捕获。难点在于很多状态是瞬态的等你想到要抓取时它已消失。我处理过一个经典案例npm run build在CI服务器上随机失败错误信息是FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory。本地复现时一切正常。问题在于V8引擎的堆内存使用是动态的node --max-old-space-size4096参数只是上限实际占用取决于GC时机。解决方案是构建状态快照触发器# 在package.json scripts中修改build命令 build: node --inspect-brk --max-old-space-size4096 ./scripts/build.js # 启动后立即在另一终端执行需安装chrome-devtools-frontend curl -s http://localhost:9229/json | jq -r .[0].devtoolsFrontendUrl | xargs open # 在DevTools Memory面板点击Take Heap Snapshot但这仅适用于开发环境。生产环境需更激进的方案内存快照在Node.js进程中注入process.on(SIGUSR2, () { require(v8).writeHeapSnapshot(); });通过kill -USR2 pid触发文件系统快照用ls -laR /app/node_modules | sha256sum生成依赖树指纹对比成功/失败构建的哈希值网络栈快照ss -tulnp监听端口、ip route show路由表、cat /proc/sys/net/ipv4/ip_local_port_range端口范围进程上下文cat /proc/pid/status内存限制、cat /proc/pid/environ | tr \0 \n环境变量全集。最关键的技巧是所有快照必须带唯一标识符。我在每个快照文件名中嵌入$(hostname)-$(date -u %Y%m%dT%H%M%S%Z)-$(git rev-parse --short HEAD)这样当发现某次构建失败时能瞬间定位到对应的全部快照集合无需在海量日志中人工筛选。2.3 依赖图谱从“我知道用什么”到“我知道它怎么用我”绝大多数故障源于对依赖关系的幻觉。你以为npm安装的openai/codex只依赖axios但axios的follow-redirects子包在Node.js 18中会尝试加载dns.promises而该API在Alpine Linux的musl libc下行为异常——这个链条只有在构建完整的依赖图谱时才会暴露。构建图谱不是简单执行npm ls --depth10而是分三层解析声明依赖Declaredpackage.json中dependencies/devDependencies字段解析依赖Resolvedpackage-lock.json中每个包的resolvedURL和integrity哈希这是npm实际安装的版本运行时依赖Runtime通过node -e console.log(require.resolve(axios))获取真实路径再用ldd检查其C扩展依赖如node_modules/node-sqlite3/lib/binding/napi-v3/node_sqlite3.node。我曾用Python脚本自动化此过程import json, subprocess, sys from pathlib import Path def build_dependency_graph(lock_file: str): with open(lock_file) as f: lock json.load(f) # 提取所有resolved包及其integrity packages {} for pkg_name, pkg_info in lock.get(packages, {}).items(): if pkg_info.get(resolved): packages[pkg_name] { resolved: pkg_info[resolved], integrity: pkg_info.get(integrity, N/A), dependencies: list(pkg_info.get(dependencies, {}).keys()) } # 检查C扩展依赖 for pkg_name, info in packages.items(): binding_path Path(node_modules) / pkg_name / lib / binding if binding_path.exists(): for so_file in binding_path.rglob(*.node): try: ldd_out subprocess.check_output([ldd, str(so_file)], stderrsubprocess.STDOUT, textTrue) info[c_deps] [line.split()[0] for line in ldd_out.splitlines() if in line and not found not in line] except: info[c_deps] [ldd_failed] return packages if __name__ __main__: graph build_dependency_graph(package-lock.json) # 输出为DOT格式供Graphviz渲染 print(digraph dependencies {) for pkg, info in graph.items(): print(f {pkg} [label{pkg}\\n{info[resolved].split(/)[-1]}];) for dep in info.get(dependencies, []): if dep in graph: print(f {pkg} - {dep};) print(})运行后生成DOT文件用dot -Tpng dependencies.dot -o deps.png可视化你会看到一个清晰的依赖网络。当某个包如heapjack-openai突然引入node-fetch3而你的主应用仍用node-fetch2图谱会立刻标红冲突节点——这比阅读npm warn eresolve overriding peer dependency警告直观百倍。注意依赖图谱必须包含版本锁定信息。npm install时若未加--no-package-lockpackage-lock.json才是真相yarn.lock同理。我见过太多团队因忽略lock文件导致CI构建与本地环境差异巨大而根本原因就是lodash从4.17.21升级到4.17.22时其内部baseIsEqual函数的递归深度限制被调整恰好触发了某处深度嵌套JSON比较的栈溢出。3. Docker环境下的hindsight实战从“virtualization support not detected”到内核模块溯源Docker Desktop在Windows上启动失败报错virtualization support not detected这是热搜词里最高频的痛点之一。但几乎所有教程都止步于“开启BIOS中的Intel VT-x/AMD-V”这就像医生只告诉病人“多喝水”却不查病因。真正的hindsight要求我们深入到Windows内核模块层面理解Hyper-V、WSL2、Docker Desktop三者如何争夺硬件虚拟化资源。3.1 故障现象的精准分层诊断首先明确该错误不是Docker Desktop独有的而是Windows Hypervisor Platform (WHPX) API调用失败的通用表现。它可能由以下四层原因导致必须逐层排除层级检查点验证命令预期输出异常含义硬件层CPU是否支持虚拟化coreinfo -v(Sysinternals工具)HYPERVISOR PRESENT行显示*CPU不支持或BIOS禁用固件层BIOS/UEFI设置重启进BIOS检查Intel VT-x/AMD-VEnabled设置未保存或被Secure Boot覆盖系统层Windows功能开关dism.exe /online /get-features | findstr hypervFeature Name : Microsoft-Hyper-V状态为EnabledHyper-V未启用或与WSL2冲突驱动层内核模块加载sc query winhvplatformSTATE : 4 RUNNINGWHPX驱动未加载或被其他安全软件拦截我处理过一个典型案例客户机器BIOS中VT-x明确开启coreinfo -v显示*但dism命令显示Hyper-V为Disabled。进一步执行bcdedit /enum firmware发现hypervisorlaunchtype为Off。执行bcdedit /set hypervisorlaunchtype auto后重启问题解决。这说明BIOS设置只是前提Windows引导配置才是最终开关。3.2 WSL2与Docker Desktop的资源争用机制当WSL2和Docker Desktop同时启用时它们都试图独占WHPX接口。Docker Desktop的错误日志里常有Failed to start WHPX: ERROR_NOT_SUPPORTED这并非驱动故障而是WHPX的互斥锁机制被WSL2抢先持有。验证方法# 查看当前WHPX占用者 Get-WinEvent -FilterHashtable {LogNameSystem; ID150} -MaxEvents 10 | Where-Object {$_.Message -match WHPX} | ForEach-Object { $_.Message }若输出包含WSL2 is using WHPX则证明争用存在。此时有两种解法方案A推荐让Docker Desktop使用WSL2后端。在Docker Desktop设置中勾选Use the WSL 2 based engine并确保WSL2发行版如Ubuntu已安装。此时Docker容器实际运行在WSL2的Linux内核中绕过WHPX争用方案B彻底禁用WSL2改用Hyper-V后端。执行wsl --unregister Ubuntu卸载WSL2然后在Docker Desktop设置中取消勾选WSL2选项并启用Use the Hyper-V backend。关键洞察Docker Desktop的“virtualization support not detected”错误90%以上的真实原因是WHPX资源被抢占而非硬件不支持。因此coreinfo -v显示*后应立即检查WSL2状态wsl -l -v若状态为Running则优先尝试方案A。3.3 容器内Python环境的hindsight取证假设Docker Desktop启动成功但容器内Python脚本调用OpenAI API时出现ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]。这通常被归咎于证书问题但hindsight要求我们确认是容器内CA证书库缺失还是Docker网络DNS解析将api.openai.com指向了错误IP或是Python的certifi包版本过旧取证步骤进入容器调试docker exec -it my-app sh验证DNS解析# 对比宿主机与容器解析结果 nslookup api.openai.com # 容器内执行 nslookup api.openai.com 1.1.1.1 # 强制使用Cloudflare DNS若前者超时或返回私有IP说明Docker DNS配置错误检查/etc/resolv.conf应为nameserver 127.0.0.11验证证书链openssl s_client -connect api.openai.com:443 -servername api.openai.com 2/dev/null | \ openssl x509 -noout -text | grep Subject:正常应显示CN *.api.openai.com验证Python证书路径import ssl, certifi print(certifi location:, certifi.where()) print(default SSL context:, ssl.create_default_context().cafile)若两者路径不一致说明Python未使用certifi证书库需设置SSL_CERT_FILE/usr/local/lib/python3.9/site-packages/certifi/cacert.pem。我曾在一个Alpine镜像中发现apk add ca-certificates安装的证书位于/etc/ssl/certs/ca-certificates.crt但Python的ssl.create_default_context()默认读取/usr/local/share/ca-certificates/导致证书验证失败。解决方案是在Dockerfile中添加RUN update-ca-certificates \ ln -sf /etc/ssl/certs/ca-certificates.crt /usr/local/share/ca-certificates/提示Alpine Linux的musl libc与glibc行为差异是Docker环境下hindsight的高频雷区。getaddrinfo()在musl中默认不支持AF_INET6当DNS返回AAAA记录时可能静默失败。验证方法getent ahosts api.openai.com若无IPv6地址输出则需在/etc/nsswitch.conf中添加hosts: files dns [!UNAVAILreturn]。4. npm生态的hindsight陷阱从peer dependency警告到构建产物污染npm warn eresolve overriding peer dependency是npm 8的默认警告多数人选择忽略直到某天npm run build产出的JS bundle体积暴涨200%线上页面白屏。这时hindsight的价值在于把警告转化为可执行的依赖治理动作。4.1 peer dependency的本质框架与插件间的契约Peer dependency不是“建议安装”而是运行时契约。以react和react-router-dom为例react-router-dom6.x声明peerDependencies: { react: ^18.0.0 }这意味着react-router-dom内部代码假设React.createElement函数签名与React 18兼容若你项目中react版本为17.0.2npm会安装react-router-dom但运行时useNavigateHook可能因React 17缺少useSyncExternalStore而崩溃。npm的eresolve警告正是在提醒你正在强制覆盖这个契约。但警告不告诉你哪个包触发了覆盖。解决方案是启用npm的详细解析日志npm install --loglevel verbose 21 | grep -E (peer|resolve|dedupe)输出中会包含类似npm verb peer dep missing: react^18.0.0, required by react-router-dom6.15.0 npm verb resolve using resolution: react17.0.2 npm verb deduping: react17.0.2 from node_modules/react这清晰表明react-router-dom需要React 18但当前解析到的是17.0.2。此时应升级react到18.x推荐或降级react-router-dom到5.x不推荐放弃新特性绝对不要用--force或--legacy-peer-deps压制警告。4.2 构建产物污染的溯源从node_modules到webpack chunk当npm run build后bundle体积异常首要怀疑对象是node_modules中混入了开发依赖。例如openai/codex的devDependencies包含typescript而其package.json中main字段指向dist/index.js但某些打包工具如Parcel会错误地将devDependencies中的typescript也打包进去因为typescript的types字段指向index.d.ts而.d.ts文件被当作类型定义加载。取证方法分析node_modules结构# 查找所有包含typescript的包 find node_modules -name typescript -type d | head -5 # 检查openai/codex的依赖树 npm ls typescript --all | grep -E (codex|typescript)检查webpack bundle内容# 生成stats.json npx webpack --profile --json stats.json # 用source-map-explorer分析 npx source-map-explorer dist/*.js若发现typescript相关模块占据大量体积证明构建工具错误打包了dev依赖。根本解决方案在package.json中为openai/codex添加bundledDependencies字段或使用pnpm替代npm——pnpm的硬链接机制天然隔离dev依赖不会将其混入生产bundle。4.3 Windows下npm.ps1执行策略的hindsight修复npm : 无法加载文件 c:\program files\nodejs\npm.ps1, 因为在此系统上禁止运行脚本是Windows PowerShell的经典错误。表面看是PowerShell执行策略问题但hindsight要求我们理解npm.cmd是Windows批处理文件它会调用node.exe执行npm-cli.jsnpm.ps1是PowerShell脚本仅当用户显式调用npm.ps1时才生效真正的错误是某些IDE如VS Code的终端默认启动PowerShell而npm命令被解析为npm.ps1而非npm.cmd。验证方法在PowerShell中执行Get-Command npm | Format-List若CommandType为Application则指向npm.cmd若为Alias则可能指向npm.ps1。修复步骤临时方案在VS Code设置中将终端默认Shell改为cmd.exe或Git Bash永久方案在PowerShell中执行# 查看当前执行策略 Get-ExecutionPolicy -List # 为当前用户设置RemoteSigned允许本地脚本 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser # 验证npm.cmd是否在PATH中 Get-Command npm | Select-Object -ExpandProperty Definition终极方案在项目根目录创建.vscode/settings.json{ terminal.integrated.defaultProfile.windows: Command Prompt, terminal.integrated.profiles.windows: { Command Prompt: { path: cmd.exe } } }注意Set-ExecutionPolicy需以管理员身份运行PowerShell但-Scope CurrentUser参数确保只影响当前用户不降低系统安全性。这是比全局禁用执行策略更安全的hindsight实践。5. OpenAI集成场景的hindsight从API Key泄露到请求链路追踪OpenAI API的429 Too Many Requests错误常被误认为“调用太频繁”但hindsight揭示90%的429源于客户端未正确实现指数退避或服务端限流策略变更未被感知。更隐蔽的风险是API Key在Docker日志或前端代码中意外泄露。5.1 API Key泄露的主动防御与被动检测Key泄露有两大途径日志泄露Python代码中print(fUsing API key: {os.getenv(OPENAI_API_KEY)})前端泄露在React组件中硬编码const apiKey sk-...经webpack打包后明文暴露。hindsight的防御策略是双轨制主动防御在CI/CD流程中加入密钥扫描。使用git-secrets预装钩子git secrets --install git secrets --register-aws git secrets --add sk-[a-zA-Z0-9]{32} # OpenAI Key正则 git secrets --add-provider -- cat .git-secrets-patterns.git-secrets-patterns文件内容# OpenAI API Key sk-[a-zA-Z0-9]{32} # OpenAI Organization ID org-[a-zA-Z0-9]{24}被动检测在Docker容器启动后自动扫描日志流# 在entrypoint.sh中添加 tail -f /var/log/app.log | grep -E sk-[a-zA-Z0-9]{32} | \ while read line; do echo ALERT: Potential API Key leak in log: $line 2 # 发送告警到Slack或邮件 curl -X POST -H Content-type: application/json \ --data {text:Key leak detected in $HOSTNAME} \ https://hooks.slack.com/services/XXX done 5.2 请求链路追踪从Python代码到OpenAI服务端当OpenAI响应延迟超过5秒你需要知道瓶颈在哪是Python进程的HTTP客户端阻塞是Docker网络DNS解析慢还是OpenAI服务端排队构建端到端追踪客户端埋点import time, logging from openai import OpenAI client OpenAI() def traced_chat_completion(**kwargs): start_time time.time() logging.info(f[TRACE] Chat start: {kwargs.get(model, unknown)}) try: response client.chat.completions.create(**kwargs) duration time.time() - start_time logging.info(f[TRACE] Chat success: {duration:.2f}s, tokens{response.usage.total_tokens}) return response except Exception as e: duration time.time() - start_time logging.error(f[TRACE] Chat error: {duration:.2f}s, {e}) raise # 使用 traced_chat_completion(modelgpt-4, messages[{role: user, content: hello}])网络层验证# 测试DNS解析时间 time nslookup api.openai.com # 测试TCP连接时间 time timeout 10 bash -c echo /dev/tcp/api.openai.com/443 # 测试TLS握手时间 time timeout 10 openssl s_client -connect api.openai.com:443 -servername api.openai.com /dev/null 21 | grep Verify return code服务端指标OpenAI提供x-ratelimit-limit-requests等响应头记录到日志response client.chat.completions.create(...) logging.info(f[RATELIMIT] Limit: {response.headers.get(x-ratelimit-limit-requests)}, fRemaining: {response.headers.get(x-ratelimit-remaining-requests)})我曾用此方法发现某次429错误并非调用超限而是x-ratelimit-reset-requests响应头显示重置时间为0意味着服务端临时关闭了该Key的请求队列。这只能通过追踪响应头才能发现。5.3 OpenAI Gym可视化协作版的hindsight适配openai gym 的可视化协作版如gymnasiumpygamestreamlit常在Docker中启动失败错误为pygame.error: No available video device。这是因为Pygame默认使用X11图形后端而Docker容器无GUI。hindsight解决方案方案AHeadless用pygame-headless替代pygame并设置环境变量ENV SDL_VIDEODRIVERdummy RUN pip install pygame-headless方案BX11转发在宿主机运行xhost local:docker容器启动时挂载-e DISPLAYhost.docker.internal:0 -v /tmp/.X11-unix:/tmp/.X11-unix方案CWeb化用gymnasium的render_modergb_array生成帧通过streamlit的st.image()实时渲染完全规避GUI依赖。关键洞察可视化失败的根本原因不是缺少显示器而是缺少图形上下文初始化。pygame.init()在无显示设备时会抛出异常但pygame.display.set_mode((1,1))在SDL_VIDEODRIVERdummy下可成功创建虚拟Surface。因此适配代码应为import os os.environ[SDL_VIDEODRIVER] dummy # 必须在import pygame前设置 import pygame pygame.init() screen pygame.display.set_mode((1, 1)) # 创建最小虚拟窗口提示在Docker中运行Pygame的终极hindsight技巧是——永远用alpine镜像搭配pygame-headless而非debian镜像配pygame。前者镜像大小50MB后者500MB且pygame-headless专为无头环境优化无任何GUI依赖。6. 构建个人hindsight工作台自动化取证脚本与知识沉淀hindsight能力不能只靠临场发挥必须固化为可复用的工作台。我基于十年经验提炼出三个核心组件取证脚本集、故障模式知识库和环境快照模板。6.1 自动化取证脚本5分钟完成全栈诊断将前述所有诊断命令封装为hindsight.sh#!/bin/bash # hindsight.sh - 全栈自动化取证脚本 TIMESTAMP$(date -u %Y%m%dT%H%M%S%Z) HOSTNAME$(hostname) OUTPUT_DIR/tmp/hindsight_${HOSTNAME}_${TIMESTAMP} mkdir -p $OUTPUT_DIR echo [INFO] Collecting system info... uname -a $OUTPUT_DIR/system.txt df -h $OUTPUT_DIR/disk.txt free -h $OUTPUT_DIR/memory.txt echo [INFO] Collecting Docker info... docker version $OUTPUT_DIR/docker_version.txt 21 docker info $OUTPUT_DIR/docker_info.txt 21 docker ps -a $OUTPUT_DIR/docker_ps.txt 21 echo [INFO] Collecting npm info... npm version $OUTPUT_DIR/npm_version.txt 21 npm ls --depth0 $OUTPUT_DIR