├── index.js ├── .gitignore ├── Makefile ├── lib ├── sls.proto └── client.js ├── package.json ├── README.md └── test ├── unit.test.js └── integration.test.js /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/client'); 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example 3 | .DS_Store 4 | coverage 5 | .nyc_output 6 | test/config.js 7 | test/figures/*.zip 8 | doc/* 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.test.js 2 | REPORTER = spec 3 | TIMEOUT = 60000 4 | PATH := ./node_modules/.bin:$(PATH) 5 | 6 | lint: 7 | @eslint --fix lib index.js test 8 | 9 | test: 10 | @mocha -t $(TIMEOUT) -R spec $(TESTS) 11 | 12 | test-es5: 13 | @mocha --compilers js:babel-register -t $(TIMEOUT) -R spec $(TESTS) 14 | 15 | test-cov: 16 | @nyc --reporter=html --reporter=text mocha -t $(TIMEOUT) -R spec $(TESTS) 17 | 18 | test-coveralls: lint 19 | @nyc mocha -t $(TIMEOUT) -R spec $(TESTS) 20 | @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) 21 | @nyc report --reporter=text-lcov | coveralls 22 | 23 | .PHONY: test test-es5 doc 24 | -------------------------------------------------------------------------------- /lib/sls.proto: -------------------------------------------------------------------------------- 1 | package sls; 2 | 3 | message Log { 4 | required uint32 Time = 1;// UNIX Time Format 5 | message Content { 6 | required string Key = 1; 7 | required string Value = 2; 8 | } 9 | repeated Content Contents = 2; 10 | optional fixed32 TimeNs = 4; 11 | } 12 | 13 | message LogTag { 14 | required string Key = 1; 15 | required string Value = 2; 16 | } 17 | 18 | message LogGroup { 19 | repeated Log Logs= 1; 20 | optional string Reserved = 2; // reserved fields 21 | optional string Topic = 3; 22 | optional string Source = 4; 23 | repeated LogTag LogTags = 6; 24 | } 25 | 26 | message LogGroupList { 27 | repeated LogGroup logGroupList = 1; 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alicloud/log", 3 | "version": "1.2.6", 4 | "description": "Aliyun Log Service Node.js SDK", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "make test" 12 | }, 13 | "author": "Jackson Tian", 14 | "license": "MIT", 15 | "dependencies": { 16 | "debug": "^2.6.8", 17 | "httpx": "^2.1.2", 18 | "kitx": "^1.2.1", 19 | "protobufjs": "^6.8.8" 20 | }, 21 | "devDependencies": { 22 | "chai": "^5.1.2", 23 | "expect.js": "^0.3.1", 24 | "mocha": "^3.5.0", 25 | "nyc": "^11.1.0" 26 | }, 27 | "files": [ 28 | "lib", 29 | "index.js" 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/aliyun/aliyun-log-nodejs-sdk.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/aliyun/aliyun-log-nodejs-sdk/issues" 37 | }, 38 | "homepage": "https://github.com/aliyun/aliyun-log-nodejs-sdk#readme" 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Integration Test 2 | 3 | 1. Create a test project 4 | 2. Create two test stores under that project, one with index configured 5 | * The first one (without log index configured) will be used to test index CRUD 6 | * The second one (with log index configured) will be used to test log retrieval 7 | 3. Run 8 | 9 | ``` 10 | ACCESS_KEY_ID= ACCESS_KEY_SECRET= TEST_PROJECT= TEST_STORE= TEST_STORE2= make test 11 | ``` 12 | 13 | ## Init Client 14 | 15 | ### Use permanent accessKey 16 | 17 | use region 18 | 19 | ```javaScript 20 | const client = new Client({ 21 | accessKeyId: "your_access_key_id", 22 | accessKeySecret: "your_access_key_secret", 23 | region: 'cn-hangzhou' 24 | }); 25 | ``` 26 | 27 | or use endpoint 28 | 29 | ```javaScript 30 | const client = new Client({ 31 | accessKeyId: "your_access_key_id", 32 | accessKeySecret: "your_access_key_secret", 33 | endpoint: 'cn-hangzhou.log.aliyuncs.com' 34 | }); 35 | ``` 36 | 37 | ### Use Credentials Provider 38 | 39 | The CredentialsProvider offers a more convenient and secure way to obtain credentials from external sources. You can retrieve these credentials from your server or other Alibaba Cloud services, and rotate them on a regular basis. 40 | 41 | ```javaScript 42 | const yourCredentialsProvider = new YourCredentialsProvider(); 43 | const client = new Client({ 44 | credentialsProvider: yourCredentialsProvider, 45 | region: 'cn-hangzhou' 46 | }); 47 | ``` 48 | 49 | The credentialsProvider implemented by yourself must be an object that has a property named `getCredentials`, and `getCredentials` is a callable function/method that returns a credentials object. 50 | 51 | The returned credentials object from `getCredentials` must not be null or undefined, and has properties named `accessKeyId`, `accessKeySecret` and `securityToken`. 52 | 53 | Here is a simple example of a credentialsProvider: 54 | 55 | ```javaScript 56 | class YourCredentialsProvider { 57 | constructor() { 58 | this.credentials = { 59 | accessKeyId: "your_access_key_id", 60 | accessKeySecret: "your_access_key_secret", 61 | securityToken: "your_security_token" 62 | }; 63 | } 64 | // The method getCredentials is called by client to get credentials for signing and authentication. 65 | // Caching and refreshing logic is required in your implementation for performance. 66 | async getCredentials() { 67 | return this.credentials; 68 | } 69 | } 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /test/unit.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const { expect } = require('chai'); 5 | const Client = require('../'); 6 | 7 | describe('Unit test', function () { 8 | it('client constructor should set endpoint correctly', function () { 9 | const client = new Client({ 10 | accessKeyId: "bq2sjzesjmo86kq35behupbq", 11 | accessKeySecret: "4fdO2fTDDnZPU/L7CHNdemB2Nsk=", 12 | region: 'cn-hangzhou' 13 | }); 14 | assert.strictEqual(client.endpoint, 'cn-hangzhou.log.aliyuncs.com'); 15 | }); 16 | 17 | it('client#_sign should sign GET requests correctly', async function () { 18 | const client = new Client({ 19 | accessKeyId: "bq2sjzesjmo86kq35behupbq", 20 | accessKeySecret: "4fdO2fTDDnZPU/L7CHNdemB2Nsk=" 21 | }); 22 | 23 | // _sign(verb, path, queries, headers) { 24 | const sign = client._sign('GET', '/logstores', { 25 | logstoreName: '', 26 | offset: '0', 27 | size: '1000' 28 | }, { 29 | date: 'Mon, 09 Nov 2015 06:11:16 GMT', 30 | 'x-log-apiversion': '0.6.0', 31 | 'x-log-signaturemethod': 'hmac-sha1' 32 | }, await client._getCredentials()); 33 | assert.strictEqual(sign, 34 | 'LOG bq2sjzesjmo86kq35behupbq:jEYOTCJs2e88o+y5F4/S5IsnBJQ='); 35 | }); 36 | 37 | it('client#_sign should sign POST requests correctly', async function () { 38 | const client = new Client({ 39 | accessKeyId: "bq2sjzesjmo86kq35behupbq", 40 | accessKeySecret: "4fdO2fTDDnZPU/L7CHNdemB2Nsk=" 41 | }); 42 | 43 | // sign(verb, path, queries, headers) { 44 | const sign = client._sign('POST', '/logstores/test-logstore', {}, { 45 | date: 'Mon, 09 Nov 2015 06:03:03 GMT', 46 | 'x-log-apiversion': '0.6.0', 47 | 'x-log-signaturemethod': 'hmac-sha1', 48 | 'content-md5': '1DD45FA4A70A9300CC9FE7305AF2C494', 49 | 'content-length': '52', 50 | 'content-type': 'application/x-protobuf', 51 | 'x-log-bodyrawsize': '50', 52 | 'x-log-compresstype': 'lz4' 53 | }, await client._getCredentials()); 54 | assert.strictEqual(sign, 55 | 'LOG bq2sjzesjmo86kq35behupbq:XWLGYHGg2F2hcfxWxMLiNkGki6g='); 56 | }); 57 | 58 | it('client#_sign should sign POST requests correctly with STS token', async function () { 59 | const credentials = { 60 | accessKeyId: 'STS.NSNYgJ2KUoYaEuDrNazRLg2a6', 61 | accessKeySecret: '56Xqw2THF5vTHTNkGWR6uGRKXToKWMi2eLFjppPNV8RR', 62 | securityToken: 'CAISiwJ1q6Ft5B2yfSjIr5D7Et3+35R02JuKR1P1lk40dt1giPfK1Dz2IHhLdXNrAuEXs/w0mmBQ7v8TlqZdVplOWU3Da+B364xK7Q75jHw5B0zwv9I+k5SANTW5KXyShb3/AYjQSNfaZY3eCTTtnTNyxr3XbCirW0ffX7SClZ9gaKZ8PGD6F00kYu1bPQx/ssQXGGLMPPK2SH7Qj3HXEVBjt3gX6wo9y9zmnZHNukGH3QOqkbVM9t6rGPX+MZkwZqUYesyuwel7epDG1CNt8BVQ/M909vccpmad5YrMUgQJuEvWa7KNo8caKgJmI7M3AbBFp/WlyKMn5raOydSrkE8cePtSVynP+g0hR0dZ+YgagAEFdb+5rO1e+OZ3kcmPKF5Zh2Sni+vF1qzKA/SElND5koQQV6uvVCweKnfzCPMKjY0OXWmfgtcwOTyJ4ABGsTGnILzBNRD/+Gdqe7wclZrj0aDUkTdFf8k7SudZuO9KOPBe8mS3pJoMs1p67mWA/J4Wn0dottbprb5EQOBRxUC6bw==' 63 | }; 64 | const client = new Client(credentials); 65 | 66 | // sign(verb, path, queries, headers) { 67 | const sign = client._sign('POST', '/logstores/test-logstore', {}, { 68 | date: 'Mon, 09 Nov 2015 06:03:03 GMT', 69 | 'x-log-apiversion': '0.6.0', 70 | 'x-log-signaturemethod': 'hmac-sha1', 71 | 'content-md5': '1DD45FA4A70A9300CC9FE7305AF2C494', 72 | 'content-length': '52', 73 | 'content-type': 'application/x-protobuf', 74 | 'x-log-bodyrawsize': '50', 75 | 'x-log-compresstype': 'lz4', 76 | 'x-acs-security-token': credentials.securityToken, 77 | }, await client._getCredentials()); 78 | assert.strictEqual(sign, 79 | 'LOG STS.NSNYgJ2KUoYaEuDrNazRLg2a6:G3R03b6PwVI+zUaLtqezsBDL/j8='); 80 | }); 81 | 82 | it('client with credentialsProvider, getCredentials is not function', async function () { 83 | expect(function () { 84 | new Client({ 85 | credentialsProvider: { 86 | getCredentials: "invalid function type" 87 | } 88 | }); 89 | }).to.throw(Error); 90 | }); 91 | it('client init with credentialsProvider has no method getCredentials', async function () { 92 | expect(function () { 93 | new Client({ 94 | credentialsProvider: {} 95 | }); 96 | }).to.throw(Error); 97 | }); 98 | it('client init with credentialsProvider has sync method getCredentials', async function () { 99 | expect(function () { 100 | new Client({ 101 | credentialsProvider: { 102 | getCredentials: function () { 103 | return { 104 | accessKeyId: "accessKeyId", 105 | accessKeySecret: "accessKeySecret" 106 | }; 107 | } 108 | } 109 | }); 110 | }).to.throw(Error); 111 | }); 112 | it('client init with credentialsProvider, getCredentials return invalid credentials', async function () { 113 | const client = new Client({ 114 | credentialsProvider: { 115 | getCredentials: async function () { 116 | return { 117 | accessKeyId: "invalid accessKeyId" 118 | }; 119 | } 120 | } 121 | }); 122 | try { 123 | await client._getCredentials(); 124 | assert.fail('Expected function to throw an error'); 125 | } catch (e) { 126 | } 127 | }); 128 | 129 | it('client init with credentialsProvider, with sts', async function () { 130 | const credentials = { 131 | accessKeyId: 'STS.NSNYgJ2KUoYaEuDrNazRLg2a6', 132 | accessKeySecret: '56Xqw2THF5vTHTNkGWR6uGRKXToKWMi2eLFjppPNV8RR', 133 | securityToken: 'CAISiwJ1q6Ft5B2yfSjIr5D7Et3+35R02JuKR1P1lk40dt1giPfK1Dz2IHhLdXNrAuEXs/w0mmBQ7v8TlqZdVplOWU3Da+B364xK7Q75jHw5B0zwv9I+k5SANTW5KXyShb3/AYjQSNfaZY3eCTTtnTNyxr3XbCirW0ffX7SClZ9gaKZ8PGD6F00kYu1bPQx/ssQXGGLMPPK2SH7Qj3HXEVBjt3gX6wo9y9zmnZHNukGH3QOqkbVM9t6rGPX+MZkwZqUYesyuwel7epDG1CNt8BVQ/M909vccpmad5YrMUgQJuEvWa7KNo8caKgJmI7M3AbBFp/WlyKMn5raOydSrkE8cePtSVynP+g0hR0dZ+YgagAEFdb+5rO1e+OZ3kcmPKF5Zh2Sni+vF1qzKA/SElND5koQQV6uvVCweKnfzCPMKjY0OXWmfgtcwOTyJ4ABGsTGnILzBNRD/+Gdqe7wclZrj0aDUkTdFf8k7SudZuO9KOPBe8mS3pJoMs1p67mWA/J4Wn0dottbprb5EQOBRxUC6bw==' 134 | }; 135 | const client = new Client({ 136 | credentialsProvider: { 137 | getCredentials: async function () { 138 | return credentials; 139 | } 140 | } 141 | }); 142 | 143 | // sign(verb, path, queries, headers) { 144 | const sign = client._sign('POST', '/logstores/test-logstore', {}, { 145 | date: 'Mon, 09 Nov 2015 06:03:03 GMT', 146 | 'x-log-apiversion': '0.6.0', 147 | 'x-log-signaturemethod': 'hmac-sha1', 148 | 'content-md5': '1DD45FA4A70A9300CC9FE7305AF2C494', 149 | 'content-length': '52', 150 | 'content-type': 'application/x-protobuf', 151 | 'x-log-bodyrawsize': '50', 152 | 'x-log-compresstype': 'lz4', 153 | 'x-acs-security-token': credentials.securityToken, 154 | }, await client._getCredentials()); 155 | assert.strictEqual(sign, 156 | 'LOG STS.NSNYgJ2KUoYaEuDrNazRLg2a6:G3R03b6PwVI+zUaLtqezsBDL/j8='); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const Client = require('../'); 5 | 6 | const testProject = process.env.TEST_PROJECT; 7 | const testStore = process.env.TEST_STORE; 8 | const testStore2 = process.env.TEST_STORE2; 9 | const accessKeyId = process.env.ACCESS_KEY_ID; 10 | const accessKeySecret = process.env.ACCESS_KEY_SECRET; 11 | const region = process.env.REGION; 12 | const PROJECT_DELAY = 1500; 13 | 14 | // Due to a bug in SLS we need to use a existing project to test log store CRUD 15 | assert.strictEqual(typeof testProject, 'string', 16 | 'set TEST_PROJECT envrinoment variable to an existing log project ' + 17 | 'before running the integration test'); 18 | // Due to the delay we must use an existing store to test log index CRUD 19 | assert.strictEqual(typeof testStore, 'string', 20 | 'set TEST_STORE envrinoment variable to an existing log store ' + 21 | 'before running the integration test'); 22 | // Due to the delay we must use an existing store with index to test log CRUD 23 | assert.strictEqual(typeof testStore2, 'string', 24 | 'set TEST_STORE2 envrinoment variable to an existing log store ' + 25 | 'with an index before running the integration test'); 26 | assert.strictEqual(typeof accessKeyId, 'string', 27 | 'set ACCESS_KEY_ID envrinoment variable before running the integration test'); 28 | assert.strictEqual(typeof accessKeySecret, 'string', 29 | 'set ACCESS_KEY_SECRET envrinoment variable before running ' + 30 | 'the integration test'); 31 | 32 | // cn-hangzhou.log.aliyuncs.com 33 | const client = new Client({ 34 | accessKeyId, 35 | accessKeySecret, 36 | region: region || 'cn-hangzhou' 37 | }); 38 | 39 | // HTTPS client for testing HTTPS protocol support 40 | const httpsClient = new Client({ 41 | accessKeyId, 42 | accessKeySecret, 43 | region: region || 'cn-hangzhou', 44 | use_https: true 45 | }); 46 | 47 | const index = { 48 | "ttl": 7, 49 | "keys": { 50 | "functionName": { 51 | "caseSensitive": false, 52 | "token": ["\n", "\t", ";", ",", "=", ":"], 53 | "type": "text" 54 | } 55 | }, 56 | }; 57 | 58 | const index2 = { 59 | "ttl": 7, 60 | "keys": { 61 | "serviceName": { 62 | "caseSensitive": false, 63 | "token": ["\n", "\t", ";", ",", "=", ":"], 64 | "type": "text" 65 | } 66 | }, 67 | }; 68 | 69 | function sleep(timeout) { 70 | return new Promise((resolve, reject) => { 71 | setTimeout(() => resolve(), timeout); 72 | }); 73 | } 74 | 75 | describe('Integration test', async function () { 76 | describe('log project CRUD', async function () { 77 | const projectName = `test-project-${Date.now()}`; 78 | 79 | it('createProject should ok', async function () { 80 | const res1 = await client.createProject(projectName, { 81 | description: 'test' 82 | }); 83 | assert.strictEqual(res1, ''); 84 | await sleep(PROJECT_DELAY); // delay of project creation 85 | const res2 = await client.getProject(projectName); 86 | assert.strictEqual(res2.projectName, projectName); 87 | assert.strictEqual(res2.description, 'test'); 88 | }); 89 | 90 | it('deleteProject should ok', async function () { 91 | const res = await client.deleteProject(projectName); 92 | assert.strictEqual(res, ''); 93 | try { 94 | await client.getProject(projectName); 95 | } catch (ex) { 96 | const res2 = assert.strictEqual(ex.code, 'ProjectNotExist'); 97 | return; 98 | } 99 | 100 | assert.fail('The log project should have been deleted'); 101 | }); 102 | }); 103 | 104 | describe('log store CRUD', async function () { 105 | const logstoreName = `test-logs-${Date.now()}`; 106 | 107 | it('createLogStore should ok', async function () { 108 | const res1 = await client.createLogStore(testProject, logstoreName, { 109 | ttl: 10, 110 | shardCount: 2 111 | }); 112 | assert.strictEqual(res1, ''); 113 | const res2 = await client.getLogStore(testProject, logstoreName); 114 | assert.strictEqual(res2.logstoreName, logstoreName); 115 | assert.strictEqual(res2.ttl, 10); 116 | }); 117 | 118 | it('listLogStore should ok', async function () { 119 | const res = await client.listLogStore(testProject); 120 | assert.strictEqual(typeof res.count, 'number'); 121 | assert.strictEqual(typeof res.total, 'number'); 122 | assert.strictEqual(Array.isArray(res.logstores), true); 123 | assert.strictEqual(res.logstores.length > 0, true); 124 | }); 125 | 126 | it('updateLogStore should ok', async function () { 127 | const res1 = await client.updateLogStore(testProject, logstoreName, { 128 | ttl: 20, 129 | shardCount: 2 130 | }); 131 | assert.strictEqual(res1, ''); 132 | const res2 = await client.getLogStore(testProject, logstoreName); 133 | assert.strictEqual(res2.logstoreName, logstoreName); 134 | assert.strictEqual(res2.ttl, 20); 135 | }); 136 | 137 | it('deleteLogStore should ok', async function () { 138 | const res = await client.deleteLogStore(testProject, logstoreName); 139 | assert.strictEqual(res, ''); 140 | try { 141 | const res2 = await client.getLogStore(testProject, logstoreName); 142 | } catch (ex) { 143 | assert.strictEqual(ex.code, 'LogStoreNotExist'); 144 | return; 145 | } 146 | 147 | assert.fail('The log store should have been deleted'); 148 | }); 149 | }); 150 | 151 | describe('log index', async function () { 152 | it('createIndex should ok', async function () { 153 | const res1 = await client.createIndex(testProject, testStore, index); 154 | const res2 = await client.getIndexConfig(testProject, testStore); 155 | // The effective TTL is always the same as the one in the 156 | // log project config, setting it here does not affect the config 157 | assert.strictEqual(typeof res2.ttl, "number"); 158 | assert.deepStrictEqual(res2.keys, index.keys); 159 | }); 160 | 161 | it('updateIndex should ok', async function () { 162 | const res1 = await client.updateIndex(testProject, testStore, index2); 163 | const res2 = await client.getIndexConfig(testProject, testStore); 164 | assert.deepStrictEqual(res2.keys, index2.keys); 165 | }); 166 | 167 | it('deleteIndex should ok', async function () { 168 | const res1 = await client.deleteIndex(testProject, testStore); 169 | assert.strictEqual(res1, ''); 170 | try { 171 | const res2 = await client.getIndexConfig(testProject, testStore); 172 | } catch (ex) { 173 | assert.strictEqual(ex.code, 'IndexConfigNotExist'); 174 | return; 175 | } 176 | 177 | assert.fail('The log index should have been deleted'); 178 | }); 179 | }); 180 | 181 | describe('getProjectLogs', async function () { 182 | const from = new Date(); 183 | from.setDate(from.getDate() - 1); 184 | const to = new Date(); 185 | 186 | it('getProjectLogs should ok', async function () { 187 | const res = await client.getProjectLogs(testProject, { 188 | query:`select count(*) as count from tengine-log where __time__ >'${Math.round(from.getTime()/1000) }' and __time__ < '${Math.round(to.getTime()/1000)}' limit 0,20`, 189 | }); 190 | assert.strictEqual(Array.isArray(res), true); 191 | }); 192 | }); 193 | 194 | describe('getLogs', async function () { 195 | const from = new Date(); 196 | from.setDate(from.getDate() - 1); 197 | const to = new Date(); 198 | 199 | it('getLogs should ok', async function () { 200 | const res = await client.getLogs(testProject, testStore2, from, to); 201 | assert.strictEqual(Array.isArray(res), true); 202 | }); 203 | }); 204 | 205 | describe('getHistograms', async function () { 206 | const from = new Date(); 207 | from.setDate(from.getDate() - 1); 208 | const to = new Date(); 209 | 210 | it('getLogs should ok', async function () { 211 | const res = await client.getHistograms(testProject, testStore2, from, to); 212 | assert.strictEqual(Array.isArray(res), true); 213 | }); 214 | }); 215 | 216 | describe('postLogStoreLogs', async function () { 217 | const logGroup = { 218 | logs: [ 219 | { content: { level: 'debug', message: 'test1-' + Date.now() }, timestamp: Math.floor(new Date().getTime() / 1000) }, 220 | { content: { level: 'info', message: 'test2-' + Date.now() }, timestamp: Math.floor(new Date().getTime() / 1000) } 221 | ], 222 | tags: [{ tag1: 'testTag' }] 223 | }; 224 | 225 | it('postLogStoreLogs should ok', async function () { 226 | const res = await client.postLogStoreLogs(testProject, testStore2, logGroup); 227 | assert.strictEqual(res, ''); 228 | }); 229 | }); 230 | 231 | describe('postLogStoreLogsWithTopicSource', async function () { 232 | const logGroup = { 233 | logs: [ 234 | { content: { level: 'debug', message: 'test1-' + Date.now() }, timestamp: Math.floor(new Date().getTime() / 1000) }, 235 | { content: { level: 'info', message: 'test2-' + Date.now() }, timestamp: Math.floor(new Date().getTime() / 1000) } 236 | ], 237 | tags: [{ tag1: 'testTag' }], 238 | topic: 'testTopic', 239 | source: 'testSource' 240 | }; 241 | 242 | it('postLogStoreLogsWithTopicSource should ok', async function () { 243 | const res = await client.postLogStoreLogs(testProject, testStore2, logGroup); 244 | assert.strictEqual(res, ''); 245 | }); 246 | }); 247 | describe('postLogStoreLogsWithTimeNs', async function () { 248 | const logGroup = { 249 | logs: [ 250 | { 251 | content: { level: 'debug', message: 'test1-' + Date.now() }, 252 | timestamp: Math.floor(new Date().getTime() / 1000), 253 | timestampNsPart: Math.floor(new Date().getTime() * 1000 * 1000) % 1000000000, 254 | }, 255 | { 256 | content: { level: 'info', message: 'test2-' + Date.now() }, timestamp: Math.floor(new Date().getTime() / 1000), 257 | timestampNsPart: Math.floor(new Date().getTime() * 1000 * 1000) % 1000000000, 258 | } 259 | ], 260 | tags: [{ tag1: 'testTag' }], 261 | topic: 'ns', 262 | source: 'ns' 263 | }; 264 | 265 | it('postLogStoreLogsWithTimeNs should ok', async function () { 266 | const res = await client.postLogStoreLogs(testProject, testStore2, logGroup); 267 | assert.strictEqual(res, ''); 268 | }); 269 | }); 270 | 271 | describe('HTTPS protocol support', async function () { 272 | it('listLogStore via HTTPS should ok', async function () { 273 | const res = await httpsClient.listLogStore(testProject); 274 | assert.strictEqual(typeof res.count, 'number'); 275 | assert.strictEqual(typeof res.total, 'number'); 276 | assert.strictEqual(Array.isArray(res.logstores), true); 277 | }); 278 | }); 279 | 280 | describe('HTTPS protocol support with endpoint', async function () { 281 | it('listLogStore via HTTPS with endpoint should ok', async function () { 282 | const client = new Client({ 283 | accessKeyId, 284 | accessKeySecret, 285 | endpoint: 'https://cn-hangzhou.log.aliyuncs.com' 286 | }); 287 | const res = await client.listLogStore(testProject); 288 | assert.strictEqual(typeof res.count, 'number'); 289 | assert.strictEqual(typeof res.total, 'number'); 290 | assert.strictEqual(Array.isArray(res.logstores), true); 291 | }); 292 | }); 293 | 294 | describe('HTTP protocol support with endpoint', async function () { 295 | it('listLogStore via HTTP with endpoint should ok', async function () { 296 | const client = new Client({ 297 | accessKeyId, 298 | accessKeySecret, 299 | endpoint: 'http://cn-hangzhou.log.aliyuncs.com' 300 | }); 301 | const res = await client.listLogStore(testProject); 302 | assert.strictEqual(typeof res.count, 'number'); 303 | assert.strictEqual(typeof res.total, 'number'); 304 | assert.strictEqual(Array.isArray(res.logstores), true); 305 | }); 306 | }); 307 | }); 308 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const querystring = require('querystring'); 5 | const assert = require('assert'); 6 | 7 | const httpx = require('httpx'); 8 | const kitx = require('kitx'); 9 | const debug = require('debug')('log:client'); 10 | 11 | // protobuf 12 | const protobuf = require("protobufjs"); 13 | const builder = protobuf.loadSync(path.join(__dirname, './sls.proto')); 14 | const LogProto = builder.lookup('sls.Log'); 15 | const LogContentProto = builder.lookup('sls.Log.Content') 16 | const LogTagProto = builder.lookupType('sls.LogTag'); 17 | const LogGroupProto = builder.lookupType('sls.LogGroup'); 18 | 19 | function getCanonicalizedHeaders(headers) { 20 | const keys = Object.keys(headers); 21 | const prefixKeys = []; 22 | for (let i = 0; i < keys.length; i++) { 23 | const key = keys[i]; 24 | if (key.startsWith('x-log-') || key.startsWith('x-acs-')) { 25 | prefixKeys.push(key); 26 | } 27 | } 28 | 29 | prefixKeys.sort(); 30 | 31 | var result = ''; 32 | for (let i = 0; i < prefixKeys.length; i++) { 33 | const key = prefixKeys[i]; 34 | result += `${key}:${String(headers[key]).trim()}\n`; 35 | } 36 | 37 | return result; 38 | } 39 | 40 | function format(value) { 41 | if (typeof value === 'undefined') { 42 | return ''; 43 | } 44 | return String(value); 45 | } 46 | 47 | function getCanonicalizedResource(path, queries) { 48 | var resource = `${path}`; 49 | const keys = Object.keys(queries); 50 | const pairs = new Array(keys.length); 51 | for (var i = 0; i < keys.length; i++) { 52 | const key = keys[i]; 53 | pairs[i] = `${key}=${format(queries[key])}`; 54 | } 55 | 56 | pairs.sort(); 57 | const querystring = pairs.join('&'); 58 | if (querystring) { 59 | resource += `?${querystring}`; 60 | } 61 | 62 | return resource; 63 | } 64 | 65 | class Client { 66 | constructor(config) { 67 | this.region = config.region; 68 | this.net = config.net; 69 | 70 | // ak 71 | this.accessKeyId = config.accessKeyId; 72 | this.accessKeySecret = config.accessKeySecret; 73 | this.securityToken = config.securityToken; 74 | this.credentialsProvider = config.credentialsProvider; 75 | this.userAgent = config.userAgent ?? 'aliyun-log-nodejs-sdk'; 76 | 77 | if (this.credentialsProvider) { 78 | if (!Client._isAsyncFunction(this.credentialsProvider.getCredentials)) { 79 | throw new Error('config.credentialsProvider must be an object with getCredentials async function'); 80 | } 81 | } else { 82 | this._validateCredentials({ 83 | accessKeyId: this.accessKeyId, 84 | accessKeySecret: this.accessKeySecret, 85 | securityToken: this.securityToken 86 | }) 87 | } 88 | 89 | this.use_https = config.use_https ?? false; 90 | // endpoint 91 | if (config.endpoint) { 92 | if (config.endpoint.startsWith("https://")) { 93 | this.endpoint = config.endpoint.slice(8); 94 | this.use_https = true; 95 | } else if (config.endpoint.startsWith("http://")) { 96 | this.endpoint = config.endpoint.slice(7); 97 | this.use_https = false; 98 | } else { 99 | this.endpoint = config.endpoint; 100 | } 101 | 102 | } else { 103 | const region = this.region; 104 | const type = this.net ? `-${this.net}` : ''; 105 | this.endpoint = `${region}${type}.log.aliyuncs.com`; 106 | } 107 | } 108 | 109 | _validateCredentials (credentials) { 110 | if (!credentials || !credentials.accessKeyId || !credentials.accessKeySecret) { 111 | throw new Error('Missing credentials or missing accessKeyId/accessKeySecret in credentials.'); 112 | } 113 | return credentials; 114 | } 115 | 116 | static _isAsyncFunction(fn) { 117 | return fn.constructor.name === 'AsyncFunction'; 118 | } 119 | 120 | async _getCredentials () { 121 | if (!this.credentialsProvider) { 122 | return { 123 | accessKeyId: this.accessKeyId, 124 | accessKeySecret: this.accessKeySecret, 125 | securityToken: this.securityToken 126 | }; 127 | } 128 | return this._validateCredentials(await this.credentialsProvider.getCredentials()); 129 | } 130 | 131 | async _request(verb, projectName, path, queries, body, headers, options) { 132 | var prefix = projectName ? `${projectName}.` : ''; 133 | var suffix = queries ? `?${querystring.stringify(queries)}` : ''; 134 | const scheme = this.use_https ? 'https' : 'http'; 135 | var url = `${scheme}://${prefix}${this.endpoint}${path}${suffix}`; 136 | 137 | const mergedHeaders = Object.assign({ 138 | 'content-type': 'application/json', 139 | 'date': new Date().toGMTString(), 140 | 'x-log-apiversion': '0.6.0', 141 | 'x-log-signaturemethod': 'hmac-sha1', 142 | 'user-agent': this.userAgent 143 | }, headers); 144 | 145 | const credentials = await this._getCredentials(); 146 | // support STS stoken 147 | if (credentials.securityToken) { 148 | mergedHeaders['x-acs-security-token'] = credentials.securityToken; 149 | } 150 | 151 | if (body) { 152 | assert(Buffer.isBuffer(body), 'body must be buffer'); 153 | mergedHeaders['content-md5'] = kitx.md5(body, 'hex').toUpperCase(); 154 | mergedHeaders['content-length'] = body.length; 155 | } 156 | 157 | // verb, path, queries, headers 158 | const sign = this._sign(verb, path, queries, mergedHeaders, credentials); 159 | mergedHeaders['authorization'] = sign; 160 | 161 | const response = await httpx.request(url, Object.assign({ 162 | method: verb, 163 | data: body, 164 | headers: mergedHeaders 165 | }, options)); 166 | 167 | var responseBody = await httpx.read(response, 'utf8'); 168 | const contentType = response.headers['content-type'] || ''; 169 | 170 | if (contentType.startsWith('application/json')) { 171 | responseBody = JSON.parse(responseBody); 172 | } 173 | 174 | if (responseBody.errorCode && responseBody.errorMessage) { 175 | var err = new Error(responseBody.errorMessage); 176 | err.code = responseBody.errorCode; 177 | err.requestid = response.headers['x-log-requestid']; 178 | err.name = `${err.code}Error`; 179 | throw err; 180 | } 181 | 182 | if (responseBody.Error) { 183 | var err = new Error(responseBody.Error.Message); 184 | err.code = responseBody.Error.Code; 185 | err.requestid = responseBody.Error.RequestId; 186 | err.name = `${err.code}Error`; 187 | throw err; 188 | } 189 | 190 | return responseBody; 191 | } 192 | 193 | _sign(verb, path, queries, headers, credentials) { 194 | const contentMD5 = headers['content-md5'] || ''; 195 | const contentType = headers['content-type'] || ''; 196 | const date = headers['date']; 197 | const canonicalizedHeaders = getCanonicalizedHeaders(headers); 198 | const canonicalizedResource = getCanonicalizedResource(path, queries); 199 | const signString = `${verb}\n${contentMD5}\n${contentType}\n` + 200 | `${date}\n${canonicalizedHeaders}${canonicalizedResource}`; 201 | debug('signString: %s', signString); 202 | const signature = kitx.sha1(signString, credentials.accessKeySecret, 'base64'); 203 | 204 | return `LOG ${credentials.accessKeyId}:${signature}`; 205 | } 206 | 207 | getProject(projectName, options) { 208 | return this._request('GET', projectName, '/', {}, null, {}, options); 209 | } 210 | 211 | getProjectLogs(projectName, data = {}, options) { 212 | return this._request('GET', projectName, '/logs', data, null, {}, options); 213 | } 214 | 215 | createProject(projectName, data, options) { 216 | const body = Buffer.from(JSON.stringify({ 217 | projectName, 218 | description: data.description 219 | })); 220 | 221 | const headers = { 222 | 'x-log-bodyrawsize': body.byteLength 223 | }; 224 | 225 | return this._request('POST', undefined, '/', {}, body, headers, options); 226 | } 227 | 228 | deleteProject(projectName, options) { 229 | const body = Buffer.from(JSON.stringify({ 230 | projectName 231 | })); 232 | 233 | const headers = { 234 | // 'x-log-bodyrawsize': body.byteLength 235 | }; 236 | 237 | return this._request('DELETE', projectName, '/', {}, body, headers, options); 238 | } 239 | 240 | // Instance methods 241 | listLogStore(projectName, data = {}, options) { 242 | const queries = { 243 | logstoreName: data.logstoreName, 244 | offset: data.offset, 245 | size: data.size 246 | }; 247 | 248 | return this._request('GET', projectName, '/logstores', queries, null, {}, options); 249 | } 250 | 251 | createLogStore(projectName, logstoreName, data = {}, options) { 252 | const body = Buffer.from(JSON.stringify({ 253 | logstoreName, 254 | ttl: data.ttl, 255 | shardCount: data.shardCount 256 | })); 257 | 258 | return this._request('POST', projectName, '/logstores', {}, body, {}, options); 259 | } 260 | 261 | deleteLogStore(projectName, logstoreName, options) { 262 | const path = `/logstores/${logstoreName}`; 263 | 264 | return this._request('DELETE', projectName, path, {}, null, {}, options); 265 | } 266 | 267 | updateLogStore(projectName, logstoreName, data = {}, options) { 268 | const body = Buffer.from(JSON.stringify({ 269 | logstoreName, 270 | ttl: data.ttl, 271 | shardCount: data.shardCount 272 | })); 273 | 274 | const path = `/logstores/${logstoreName}`; 275 | 276 | return this._request('PUT', projectName, path, {}, body, {}, options); 277 | } 278 | 279 | getLogStore(projectName, logstoreName, options) { 280 | const path = `/logstores/${logstoreName}`; 281 | 282 | return this._request('GET', projectName, path, {}, null, {}, options); 283 | } 284 | 285 | getIndexConfig(projectName, logstoreName, options) { 286 | const path = `/logstores/${logstoreName}/index`; 287 | 288 | return this._request('GET', projectName, path, {}, null, {}, options); 289 | } 290 | 291 | createIndex(projectName, logstoreName, index, options) { 292 | const body = Buffer.from(JSON.stringify(index)); 293 | 294 | const headers = { 295 | 'x-log-bodyrawsize': body.byteLength 296 | }; 297 | const path = `/logstores/${logstoreName}/index`; 298 | 299 | return this._request('POST', projectName, path, {}, body, headers, options); 300 | } 301 | 302 | updateIndex(projectName, logstoreName, index, options) { 303 | const body = Buffer.from(JSON.stringify(index)); 304 | 305 | const headers = { 306 | 'x-log-bodyrawsize': body.byteLength 307 | }; 308 | const path = `/logstores/${logstoreName}/index`; 309 | 310 | return this._request('PUT', projectName, path, {}, body, headers, options); 311 | } 312 | 313 | deleteIndex(projectName, logstoreName, options) { 314 | const path = `/logstores/${logstoreName}/index`; 315 | 316 | return this._request('DELETE', projectName, path, {}, null, {}, options); 317 | } 318 | 319 | getLogs(projectName, logstoreName, from, to, data = {}, options) { 320 | const query = Object.assign({}, data, { 321 | type: 'log', 322 | from: Math.floor(from.getTime() / 1000), 323 | to: Math.floor(to.getTime() / 1000) 324 | }); 325 | const path = `/logstores/${logstoreName}`; 326 | return this._request('GET', projectName, path, query, null, {}, options); 327 | } 328 | 329 | getHistograms(projectName, logstoreName, from, to, data = {}, options) { 330 | const query = Object.assign({}, data, { 331 | type: 'histogram', 332 | from: Math.floor(from.getTime() / 1000), 333 | to: Math.floor(to.getTime() / 1000) 334 | }); 335 | const path = `/logstores/${logstoreName}`; 336 | return this._request('GET', projectName, path, query, null, {}, options); 337 | } 338 | 339 | postLogStoreLogs(projectName, logstoreName, data = {}, options) { 340 | const path = `/logstores/${logstoreName}/shards/lb`; 341 | if (!Array.isArray(data.logs)) { 342 | throw new Error('data.logs must be array!') 343 | } 344 | // add logs 345 | const payload = { 346 | Logs: data.logs.map(log => { 347 | let logPayload = { 348 | Time: log.timestamp, 349 | Contents: Object.entries(log.content).map(([Key, Value]) => { 350 | const logContentPayload = { Key, Value }; 351 | const err = LogContentProto.verify(logContentPayload); 352 | if (err) throw err; 353 | return logContentPayload 354 | }) 355 | } 356 | if (log.timestampNsPart !== undefined) { 357 | logPayload.TimeNs = log.timestampNsPart 358 | } 359 | const err = LogProto.verify(logPayload); 360 | if (err) throw err; 361 | return logPayload; 362 | }) 363 | }; 364 | // add tags 365 | if (Array.isArray(data.tags)) { 366 | payload.LogTags = data.tags.reduce((tags, tag) => { 367 | Object.entries(tag).forEach(([Key, Value]) => { 368 | const tagPayload = { Key, Value }; 369 | const err = LogTagProto.verify(tagPayload); 370 | if (err) throw err; 371 | tags.push(tagPayload) 372 | }); 373 | return tags; 374 | }, []); 375 | } 376 | if (data.topic && typeof data.topic === 'string') { 377 | payload.Topic = data.topic; 378 | } 379 | if (data.source && typeof data.source === 'string') { 380 | payload.Source = data.source; 381 | } 382 | const err = LogGroupProto.verify(payload); 383 | if (err) throw new Error(err); 384 | let body = LogGroupProto.create(payload); 385 | body = LogGroupProto.encode(body).finish(); 386 | const rawLength = body.byteLength; 387 | const headers = { 388 | 'x-log-bodyrawsize': rawLength, 389 | 'content-type': 'application/x-protobuf' 390 | }; 391 | return this._request('POST', projectName, path, {}, body, headers, options); 392 | } 393 | } 394 | 395 | module.exports = Client; 396 | --------------------------------------------------------------------------------