项目背景
SLS 的 命令行工具 aliyunlog 功能非常丰富,能够实现对 logstore、project、shard、index 等资源的管理操作,支持了数十条操作命令。使用也非常简单,通过 python 安装命令行工具之后,配置 AK 和region 信息即可通过简单的命令行来进行操作。
在实际项目使用中却遇到了一些问题。
发布问题
项目中进行埋点后,需要在 SLS 控制台配置报表并进行查看。正常的发布顺序为:
日常logstore(开发调试)->预发logstore(测试验证)->生产logstore(用户使用)
所以每当报表或者索引有修改时,会先在日常环境进行配置和调试,通过后再同步到预发环境让测试同学验证,验证通过后再同步到线上生产环境。
如果每次同步都是通过手动配置的话会有2个问题:
1、效率非常低,预发同步1次,生产环境因为有n个区域,所以要同步 n 次,n+1次重复劳动。
2、容易出错,手动同步一个复制粘贴搞错了很可能会导致报表不可用。
脚本封装
因为需要同步的索引和报表都属于项目,所以一开始考虑使用 aliyunlog 的 copy_logstore 命令,但实际在执行过程中发现命令行既不报错,索引和报表也没有同步成功。所以干脆自己写个脚本来实现 SLS 相关资源的发布功能。
考虑到本人对 JavaScript 比较熟悉,再加上其优秀的并发能力,所以考虑用 Node.js 编写一个脚本来实现。
以终为始,首先我们要考虑最终希望实现的具体效果,那就是把一个环境的配置信息发布到另一个环境。所以只用支持两个参数,一个是待发布的环境,一个是源环境,像下面这样:
node xxx.js [from_logstore] [to_logstore]
实际的执行过程应该是从源环境中读取报表和索引,然后更新到目标环境。这些信息就需要通过 aliyunlog 来获取了,aliyunlog 所依赖的参数我们写到对应的配置文件中,格式类似下面这样:
{ "daily": { "host": "hsot1", "project": "project1", "logstore": "store1", "AccessKeyID": "LT******", "AccessKeySecret": "zl******" }, "pre": { "host": "host2", "project": "project2", "logstore": "store2", "AccessKeyID": "LT***", "AccessKeySecret": "ny***" }, "product": [ { "host": "host3", "project": "project3", "logstore": "store3", "AccessKeyID": "LT***", "AccessKeySecret": "vgU***" }, { "host": "host4", "project": "project4", "logstore": "store4", "AccessKeyID": "LT***", "AccessKeySecret": "vg***" }, ] }
这里由于同步是单向的,不可能从生产环境同步到预发环境,同时生产环境有多个区域,所以 project 写成数组形式。
接下来考虑如何同步。
1 同步索引。获取源 logstore 的索引,然后同步到目标 logstore。
2 同步报表。报表相对索引略微复杂一些,因为一个 logstore 下会有多个报表,为了高效需要并发执行,同步之前也需要注意修改项目信息。
3 删除配置文件。为了保证对下次操作不造成影响,需要删除临时生成的配置文件
完整的代码如下:
constenv=require('./.env.sls.json'); const { execSync, exec } =require('child_process'); const { join } =require('path'); const { writeFileSync } =require('fs'); // eslint-disable-next-line no-unused-varsconst [_nodePath, _filePath, from, to] =process.argv; constsource=env[from]; constdistList=Array.isArray(env[to]) ?env[to] : [env[to]]; (async () => { // 创建临时目录execSync('mkdir -p ./conf'); awaitPromise.allSettled(updateIndex(source, distList)); console.log('step1 updatedIndex') constids=list(); constdashboards=awaitdownload(ids); console.log('step2 download dashboard') try { awaitPromise.allSettled(upload(dashboards)); } catch(e) { console.error(e); } console.log('step3 update dashboard') execSync('rm ./conf/*') })(); functionupdateIndex(){ letcmd=`aliyunlog log get_index_config --project_name=${source.project}--logstore_name=${source.logstore}--access-id=${source.AccessKeyID}--access-key=${source.AccessKeySecret}--region-endpoint=${source.host}--format-output=json,no_escape`; constindex=JSON.parse(execSync(cmd).toString()); writeFileSync(`./conf/index.json`, JSON.stringify(index, null, 2)); constfile=`file://${join(__dirname, `./conf/index.json`)}`; returndistList.map(dist=>newPromise((resolve, reject) => { cmd=`aliyunlog log create_index --project_name=${dist.project}--logstore_name=${dist.logstore}--access-id=${dist.AccessKeyID}--access-key=${dist.AccessKeySecret}--region-endpoint=${dist.host}--format-output=json,no_escape --index_detail=${file}`; exec(cmd, error=> { if(error) { cmd=`aliyunlog log update_index --project_name=${dist.project}--logstore_name=${dist.logstore}--access-id=${dist.AccessKeyID}--access-key=${dist.AccessKeySecret}--region-endpoint=${dist.host}--format-output=json,no_escape --index_detail=${file}`; returnexec(cmd, err=> { if(err) { reject(err); console.log(err); } console.log(`Updated index for host ${dist.host}`) resolve(); }) } console.log(`Created index for host ${dist.host}`) resolve() }) })) } functionlist() { constcmd=`aliyunlog log list_dashboard --project=${source.project}--access-id=${source.AccessKeyID}--access-key=${source.AccessKeySecret}--region-endpoint=${source.host}--format-output=json,no_escape`; const { dashboardItems } =JSON.parse(execSync(cmd).toString()); returndashboardItems .filter((it) =>/^dashboard/.test(it.dashboardName)) .map((it) =>it.dashboardName); } functiondownload(dashboards) { returnPromise.all( dashboards.map((it) => { returnnewPromise((resolve, reject) => { constcmd=`aliyunlog log get_dashboard --project=${source.project}\--entity=${it}--access-id=${source.AccessKeyID}--access-key=${source.AccessKeySecret}--region-endpoint=${source.host}--format-output=json,no_escape`; exec(cmd, (error, stdout) => { if (error) returnreject(error); resolve(JSON.parse(stdout.toString())); }); }); }), ); } functionupload(dashboards) { returndashboards.map((it) => { returnnewPromise((resolve, reject) => { distList.forEach((dist) => { constfilePath=`./conf/dashboard-${newDate().getTime()}.${Math.random().toString(36)}.json`try { constvalue=replaceProjectName(it, dist.project); writeFileSync(filePath, JSON.stringify(value, null, 2)); } catch (e) { console.error('writeFileSync:', e); } constfile=`file://${join(__dirname, filePath)}`; constcmd=`aliyunlog log create_dashboard --project=${dist.project}\--detail=${file}--access-id=${dist.AccessKeyID}--access-key=${dist.AccessKeySecret}--region-endpoint=${dist.host}`; exec(cmd, (error, stdout) => { if (error) { constcmd2=`aliyunlog log update_dashboard --project=${dist.project}\--detail=${file}--access-id=${dist.AccessKeyID}--access-key=${dist.AccessKeySecret}--region-endpoint=${dist.host}`; returnexec(cmd2, (err, out) => { if (err) returnreject(err); elseconsole.info(`Updated dashboards for host ${dist.host}.`); resolve(out.toString()); }); } else { console.info(`Created dashboards for host ${dist.host}.`); } resolve(stdout.toString()); }); }); }); }); } functionreplaceProjectName(obj, value) { constKEY='project'; for (constpropinobj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { if (prop===KEY) { obj[prop] =value; } elseif (typeofobj[prop] ==='object') { replaceProjectName(obj[prop], value); } } } returnobj; }