Skip to content

Commit f4f21b4

Browse files
authored
feat(logs): add logs fetching and streaming (#23)
* feat(logs): new function to only load one page of logs * feat(logs): adds a streaming class for logs that polls for now * feat(logs): adds convenience func, page size and function filter * chore: updates package.json Fixes reset-dependencies npm script and adds a watch script for continuous compilation. * refactor(./src/streams/logs.ts): logsStream refactor Should pass a client to LogsStream instead of credentials, as that's how everything else works. Access through the client should set the client. Also, changed how filtering old logs out on subsequent polls works to contain memory. * feat(./src/api/logs.ts): adds filters to listOnePageLogResources Adds StartDate, EndDate amd PageToken as options to listOnePageLogResources. * feat(client): adds getLogs function to client so that twilio-run can get a single page of recent logs * improvement(logs): turn filters for logs into an object argument * improvement(logs): make polling frequency settable for log streams * improvement(logs): in objectmode readable streams can push objects This saves serializing and deserializing the data. * improvement(logs): removed unused _buffer variable
1 parent f201cda commit f4f21b4

File tree

7 files changed

+237
-3
lines changed

7 files changed

+237
-3
lines changed

Diff for: package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"jest": "jest",
99
"test": "run-s build:noemit jest",
1010
"build": "tsc",
11+
"watch": "tsc --watch",
1112
"build:noemit": "tsc --noEmit",
1213
"postbuild": "npm shrinkwrap",
1314
"docs": "typedoc --options typedoc.json",
@@ -16,7 +17,7 @@
1617
"lint-staged": "lint-staged",
1718
"cm": "git-cz",
1819
"release": "HUSKY_SKIP_HOOKS=1 standard-version",
19-
"reset-dependencies": "bash ./hooks/update-dependencies"
20+
"reset-dependencies": "bash ./hooks/update-dependencies.sh"
2021
},
2122
"publishConfig": {
2223
"access": "public"

Diff for: src/api/functions.ts

+11
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,14 @@ export async function uploadFunction(
180180
const version = await createFunctionVersion(fn, serviceSid, client);
181181
return version.sid;
182182
}
183+
184+
/**
185+
* Checks if a string is an function SID by checking its prefix and length
186+
*
187+
* @export
188+
* @param {string} str the string to check
189+
* @returns
190+
*/
191+
export function isFunctionSid(str: string) {
192+
return str.startsWith('ZH') && str.length === 34;
193+
}

Diff for: src/api/logs.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @module @twilio-labs/serverless-api/dist/api */
22

33
import debug from 'debug';
4-
import { GotClient, LogApiResource, LogList, Sid } from '../types';
4+
import { GotClient, LogApiResource, LogList, Sid, LogFilters } from '../types';
55
import { getPaginatedResource } from './utils/pagination';
66

77
const log = debug('twilio-serverless-api:logs');
@@ -30,6 +30,49 @@ export async function listLogResources(
3030
}
3131
}
3232

33+
/**
34+
* Calls the API to retrieve a list of all assets
35+
*
36+
* @param {Sid} environmentSid environment in which to get logs
37+
* @param {Sid} serviceSid service to look for logs
38+
* @param {GotClient} client API client
39+
* @returns {Promise<LogApiResource[]>}
40+
*/
41+
export async function listOnePageLogResources(
42+
environmentSid: Sid,
43+
serviceSid: Sid,
44+
client: GotClient,
45+
filters: LogFilters
46+
): Promise<LogApiResource[]> {
47+
const pageSize = filters.pageSize || 50;
48+
const { functionSid, startDate, endDate, pageToken } = filters;
49+
try {
50+
let url = `/Services/${serviceSid}/Environments/${environmentSid}/Logs?PageSize=${pageSize}`;
51+
if (typeof functionSid !== 'undefined') {
52+
url += `&FunctionSid=${functionSid}`;
53+
}
54+
if (typeof startDate !== 'undefined') {
55+
url += `&StartDate=${
56+
startDate instanceof Date ? startDate.toISOString() : startDate
57+
}`;
58+
}
59+
if (typeof endDate !== 'undefined') {
60+
url += `&EndDate=${
61+
endDate instanceof Date ? endDate.toISOString() : endDate
62+
}`;
63+
}
64+
if (typeof pageToken !== 'undefined') {
65+
url += `&PageToken=${pageToken}`;
66+
}
67+
const resp = await client.get(url);
68+
const content = (resp.body as unknown) as LogList;
69+
return content.logs as LogApiResource[];
70+
} catch (err) {
71+
log('%O', err);
72+
throw err;
73+
}
74+
}
75+
3376
/**
3477
* Calls the API to retrieve a list of all assets
3578
*

Diff for: src/client.ts

+79-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ import {
2020
isEnvironmentSid,
2121
listEnvironments,
2222
} from './api/environments';
23-
import { getOrCreateFunctionResources, uploadFunction } from './api/functions';
23+
import {
24+
getOrCreateFunctionResources,
25+
uploadFunction,
26+
isFunctionSid,
27+
listFunctionResources,
28+
} from './api/functions';
2429
import { createService, findServiceSid, listServices } from './api/services';
2530
import {
2631
listVariablesForEnvironment,
@@ -37,9 +42,13 @@ import {
3742
GotClient,
3843
ListConfig,
3944
ListResult,
45+
LogApiResource,
46+
LogsConfig,
4047
} from './types';
4148
import { DeployStatus } from './types/consts';
4249
import { getListOfFunctionsAndAssets, SearchConfig } from './utils/fs';
50+
import { LogsStream } from './streams/logs';
51+
import { listOnePageLogResources } from './api/logs';
4352

4453
const log = debug('twilio-serverless-api:client');
4554

@@ -197,6 +206,75 @@ export class TwilioServerlessApiClient extends events.EventEmitter {
197206
return result;
198207
}
199208

209+
async getLogsStream(logsConfig: LogsConfig): Promise<LogsStream> {
210+
let { serviceSid, environment, filterByFunction } = logsConfig;
211+
if (!isEnvironmentSid(environment)) {
212+
const environmentResource = await getEnvironmentFromSuffix(
213+
environment,
214+
serviceSid,
215+
this.client
216+
);
217+
environment = environmentResource.sid;
218+
}
219+
220+
if (filterByFunction && !isFunctionSid(filterByFunction)) {
221+
const availableFunctions = await listFunctionResources(
222+
serviceSid,
223+
this.client
224+
);
225+
const foundFunction = availableFunctions.find(
226+
fn => fn.friendly_name === filterByFunction
227+
);
228+
if (!foundFunction) {
229+
throw new Error('Invalid Function Name or SID');
230+
}
231+
filterByFunction = foundFunction.sid;
232+
}
233+
const logsStream = new LogsStream(
234+
environment,
235+
serviceSid,
236+
this.client,
237+
logsConfig
238+
);
239+
240+
return logsStream;
241+
}
242+
243+
async getLogs(logsConfig: LogsConfig): Promise<LogApiResource[]> {
244+
let { serviceSid, environment, filterByFunction } = logsConfig;
245+
if (!isEnvironmentSid(environment)) {
246+
const environmentResource = await getEnvironmentFromSuffix(
247+
environment,
248+
serviceSid,
249+
this.client
250+
);
251+
environment = environmentResource.sid;
252+
}
253+
254+
if (filterByFunction && !isFunctionSid(filterByFunction)) {
255+
const availableFunctions = await listFunctionResources(
256+
serviceSid,
257+
this.client
258+
);
259+
const foundFunction = availableFunctions.find(
260+
fn => fn.friendly_name === filterByFunction
261+
);
262+
if (!foundFunction) {
263+
throw new Error('Invalid Function Name or SID');
264+
}
265+
filterByFunction = foundFunction.sid;
266+
}
267+
268+
try {
269+
return listOnePageLogResources(environment, serviceSid, this.client, {
270+
pageSize: 50,
271+
functionSid: filterByFunction,
272+
});
273+
} catch (e) {
274+
throw e;
275+
}
276+
}
277+
200278
/**
201279
* "Activates" a build by taking a specified build SID or a "source environment"
202280
* and activating the same build in the specified `environment`.

Diff for: src/streams/logs.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Readable } from 'stream';
2+
import { listOnePageLogResources } from '../api/logs';
3+
import { LogApiResource, Sid, GotClient } from '../types';
4+
import { LogsConfig } from '../types/logs';
5+
6+
export class LogsStream extends Readable {
7+
private _pollingFrequency: number;
8+
private _interval: NodeJS.Timeout | undefined;
9+
private _viewedSids: Set<Sid>;
10+
11+
constructor(
12+
private environmentSid: Sid,
13+
private serviceSid: Sid,
14+
private client: GotClient,
15+
private config: LogsConfig
16+
) {
17+
super({ objectMode: true });
18+
this._interval = undefined;
19+
this._viewedSids = new Set();
20+
this._pollingFrequency = config.pollingFrequency || 1000;
21+
}
22+
23+
set pollingFrequency(frequency: number) {
24+
this._pollingFrequency = frequency;
25+
if (this.config.tail && this._interval) {
26+
clearInterval(this._interval);
27+
this._interval = setInterval(() => {
28+
this._poll();
29+
}, this._pollingFrequency);
30+
}
31+
}
32+
33+
async _poll() {
34+
try {
35+
const logs = await listOnePageLogResources(
36+
this.environmentSid,
37+
this.serviceSid,
38+
this.client,
39+
{
40+
functionSid: this.config.filterByFunction,
41+
pageSize: this.config.limit,
42+
}
43+
);
44+
logs
45+
.filter(log => !this._viewedSids.has(log.sid))
46+
.reverse()
47+
.forEach(log => {
48+
this.push(log);
49+
});
50+
// Replace the set each time rather than adding to the set.
51+
// This way the set is always the size of a page of logs and the next page
52+
// will either overlap or not. This is instead of keeping an ever growing
53+
// set of viewSids which would cause memory issues for long running log
54+
// tails.
55+
this._viewedSids = new Set(logs.map(log => log.sid));
56+
if (!this.config.tail) {
57+
this.push(null);
58+
}
59+
} catch (err) {
60+
this.destroy(err);
61+
}
62+
}
63+
64+
_read() {
65+
if (this.config.tail && !this._interval) {
66+
this._interval = setInterval(() => {
67+
this._poll();
68+
}, this._pollingFrequency);
69+
} else {
70+
this._poll();
71+
}
72+
}
73+
74+
_destroy() {
75+
if (this._interval) {
76+
clearInterval(this._interval);
77+
this._interval = undefined;
78+
}
79+
}
80+
}

Diff for: src/types/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './deploy';
66
export * from './generic';
77
export * from './list';
88
export * from './serverless-api';
9+
export * from './logs';

Diff for: src/types/logs.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/** @module @twilio-labs/serverless-api */
2+
3+
import { Sid } from './serverless-api';
4+
5+
export type LogsConfig = {
6+
serviceSid: Sid;
7+
environment: string | Sid;
8+
tail: boolean;
9+
limit?: number;
10+
filterByFunction?: string | Sid;
11+
pollingFrequency?: number;
12+
};
13+
14+
export type LogFilters = {
15+
pageSize?: number;
16+
functionSid?: Sid;
17+
startDate?: string | Date;
18+
endDate?: string | Date;
19+
pageToken?: string;
20+
};

0 commit comments

Comments
 (0)