Add vlogs sdk for ts

This commit is contained in:
Sambo Chea 2023-05-27 11:16:06 +07:00
commit 430d052865
Signed by: sombochea
GPG Key ID: 3C7CF22A05D95490
12 changed files with 4695 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
dist/
build/
node_modules/

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# vLogs SDK for JS/TS
A simple way to collect logs and send to the server via simple SDK.
- [x] Collect the logs
- [ ] Support local retries
## Usages
```typescript
const APP_ID = 'xxx';
const API_KEY = 'vlogs_xxx';
const sdk = VLogs.create(
VLogsOptions.builder().appId(APP_ID).apiKey(API_KEY).build()
);
const request = Collector.builder()
.message('Hello from vlogs-ts-sdk')
.type(CollectorType.Log)
.source(CollectorSource.Web)
.build();
const response = await sdk.collect(request);
console.log('Request: ', request);
console.log('Response: ', response);
```
### Contributors
- Sambo Chea <sombochea@cubetiqs.com>

9
jest.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
modulePathIgnorePatterns: ['<rootDir>/dist/'],
testPathIgnorePatterns: ["/node_modules/"],
moduleNameMapper: {
"\\.(css|less)$": "identity-obj-proxy"
}
};

3687
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "@cubetiq/vlogs",
"version": "1.0.0",
"description": "A simple way to collect logs and send to the server via simple SDK.",
"main": "dist/index.js",
"dist": {
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "tsc",
"dev": "tsc -w",
"start": "node dist/index.js",
"test": "jest"
},
"repository": {
"type": "git",
"url": "git+https://github.com/CUBETIQ/vlogs_sdk_ts.git"
},
"keywords": [
"vlogs"
],
"author": "Sambo Chea <sombochea@cubetiqs.com>",
"license": "ISC",
"bugs": {
"url": "https://github.com/CUBETIQ/vlogs_sdk_ts/issues"
},
"homepage": "https://github.com/CUBETIQ/vlogs_sdk_ts#readme",
"devDependencies": {
"@types/jest": "^29.5.1",
"@types/node": "^20.2.5",
"@types/uuid": "^9.0.1",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"typescript": "^5.0.4"
},
"dependencies": {
"axios": "^1.4.0",
"uuid": "^9.0.0"
}
}

2
src/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './model'
export * from './vlgos'

692
src/model.ts Normal file
View File

@ -0,0 +1,692 @@
import { generateUUID } from "./util";
enum CollectorType {
Error,
Event,
Metric,
Trace,
Log,
Span,
}
enum CollectorSource {
Web,
Mobile,
Server,
Desktop,
IoT,
Other,
}
enum TelegramParseMode {
Markdown,
MarkdownV2,
HTML,
}
interface TelegramOptions {
token?: string;
chatId?: string;
parseMode?: TelegramParseMode;
disabled?: boolean;
extras?: any;
}
class Telegram {
token?: string;
chatId?: string;
parseMode?: TelegramParseMode;
disabled?: boolean;
extras?: any;
constructor(options: TelegramOptions = {}) {
this.token = options.token;
this.chatId = options.chatId;
this.parseMode = options.parseMode;
this.disabled = options.disabled;
this.extras = options.extras;
}
toMap(): Record<string, any> {
return {
'token': this.token,
'chat_id': this.chatId,
'parse_mode': this.parseMode,
'disabled': this.disabled,
'extras': this.extras,
};
}
static builder(): TelegramBuilder {
return new TelegramBuilder();
}
}
class TelegramBuilder {
private _token?: string;
private _chatId?: string;
private _parseMode?: TelegramParseMode;
private _disabled?: boolean;
private _extras?: any;
constructor() { }
token(token?: string): TelegramBuilder {
this._token = token;
return this;
}
chatId(chatId?: string): TelegramBuilder {
this._chatId = chatId;
return this;
}
parseMode(parseMode?: TelegramParseMode): TelegramBuilder {
this._parseMode = parseMode;
return this;
}
disabled(disabled?: boolean): TelegramBuilder {
this._disabled = disabled;
return this;
}
extras(extras?: any): TelegramBuilder {
this._extras = extras;
return this;
}
build(): Telegram {
return new Telegram({
token: this._token,
chatId: this._chatId,
parseMode: this._parseMode,
disabled: this._disabled,
extras: this._extras,
});
}
}
class Discord {
webhookId?: string;
webhookToken?: string;
webhookUrl?: string;
disabled?: boolean;
extras?: any;
constructor({
webhookId,
webhookToken,
webhookUrl,
disabled,
extras,
}: {
webhookId?: string;
webhookToken?: string;
webhookUrl?: string;
disabled?: boolean;
extras?: any;
}) {
this.webhookId = webhookId;
this.webhookToken = webhookToken;
this.webhookUrl = webhookUrl;
this.disabled = disabled;
this.extras = extras;
}
toMap(): Record<string, any> {
return {
webhook_id: this.webhookId,
webhook_token: this.webhookToken,
webhook_url: this.webhookUrl,
disabled: this.disabled,
extras: this.extras,
};
}
static builder(): DiscordBuilder {
return new DiscordBuilder();
}
}
class DiscordBuilder {
private _webhookId?: string;
private _webhookToken?: string;
private _webhookUrl?: string;
private _disabled?: boolean;
private _extras?: any;
constructor() { }
webhookId(webhookId: string | undefined): DiscordBuilder {
this._webhookId = webhookId;
return this;
}
webhookToken(webhookToken: string | undefined): DiscordBuilder {
this._webhookToken = webhookToken;
return this;
}
webhookUrl(webhookUrl: string | undefined): DiscordBuilder {
this._webhookUrl = webhookUrl;
return this;
}
disabled(disabled: boolean | undefined): DiscordBuilder {
this._disabled = disabled;
return this;
}
extras(extras: any): DiscordBuilder {
this._extras = extras;
return this;
}
build(): Discord {
return new Discord({
webhookId: this._webhookId,
webhookToken: this._webhookToken,
webhookUrl: this._webhookUrl,
disabled: this._disabled,
extras: this._extras,
});
}
}
class SDKInfo {
name?: string;
version?: string;
versionCode?: string;
hostname?: string;
sender?: string;
constructor({
name,
version,
versionCode,
hostname,
sender,
}: {
name?: string;
version?: string;
versionCode?: string;
hostname?: string;
sender?: string;
}) {
this.name = name;
this.version = version;
this.versionCode = versionCode;
this.hostname = hostname;
this.sender = sender;
}
toMap(): Record<string, any> {
return {
name: this.name,
version: this.version,
version_code: this.versionCode,
hostname: this.hostname,
sender: this.sender,
};
}
static builder(): SDKInfoBuilder {
return new SDKInfoBuilder();
}
}
class SDKInfoBuilder {
private _name?: string;
private _version?: string;
private _versionCode?: string;
private _hostname?: string;
private _sender?: string;
constructor() { }
name(name: string | undefined): SDKInfoBuilder {
this._name = name;
return this;
}
version(version: string | undefined): SDKInfoBuilder {
this._version = version;
return this;
}
versionCode(versionCode: string | undefined): SDKInfoBuilder {
this._versionCode = versionCode;
return this;
}
hostname(hostname: string | undefined): SDKInfoBuilder {
this._hostname = hostname;
return this;
}
sender(sender: string | undefined): SDKInfoBuilder {
this._sender = sender;
return this;
}
build(): SDKInfo {
return new SDKInfo({
name: this._name,
version: this._version,
versionCode: this._versionCode,
hostname: this._hostname,
sender: this._sender,
});
}
}
class Target {
telegram?: Telegram;
discord?: Discord;
sdkInfo?: SDKInfo;
constructor({
telegram,
discord,
sdkInfo,
}: {
telegram?: Telegram;
discord?: Discord;
sdkInfo?: SDKInfo;
}) {
this.telegram = telegram;
this.discord = discord;
this.sdkInfo = sdkInfo;
}
toMap(): Record<string, any> {
return {
telegram: this.telegram?.toMap(),
discord: this.discord?.toMap(),
sdk_info: this.sdkInfo?.toMap(),
};
}
merge(defaultTarget?: Target): void {
if (!defaultTarget) return;
this.telegram ||= defaultTarget.telegram;
this.discord ||= defaultTarget.discord;
}
static withTelegram(
chatId: string,
{
token,
parseMode,
disabled,
extras,
}: {
token?: string;
parseMode?: TelegramParseMode;
disabled?: boolean;
extras?: any;
} = {}
): Target {
return new Target({
telegram: new Telegram({
chatId,
token,
parseMode,
disabled,
extras,
}),
});
}
static withDiscord(
webhookUrl: string,
{
webhookId,
webhookToken,
disabled,
extras,
}: {
webhookId?: string;
webhookToken?: string;
disabled?: boolean;
extras?: any;
}
): Target {
return new Target({
discord: new Discord({
webhookUrl,
webhookId,
webhookToken,
disabled,
extras,
}),
});
}
static builder(): TargetBuilder {
return new TargetBuilder();
}
}
class TargetBuilder {
private _telegram?: Telegram;
private _discord?: Discord;
private _sdkInfo?: SDKInfo;
constructor() { }
telegram(telegram?: Telegram): TargetBuilder {
this._telegram = telegram;
return this;
}
discord(discord?: Discord): TargetBuilder {
this._discord = discord;
return this;
}
sdkInfo(sdkInfo?: SDKInfo): TargetBuilder {
this._sdkInfo = sdkInfo;
return this;
}
build(): Target {
return new Target({
telegram: this._telegram,
discord: this._discord,
sdkInfo: this._sdkInfo,
});
}
}
class Collector {
id?: string;
type?: string;
source?: string;
message?: string;
data?: any;
userAgent?: string;
timestamp?: number;
target?: Target;
tags?: string[];
constructor({
id,
type,
source,
message,
data,
userAgent,
timestamp,
target,
tags,
}: {
id?: string;
type?: string;
source?: string;
message?: string;
data?: any;
userAgent?: string;
timestamp?: number;
target?: Target;
tags?: string[];
}) {
this.id = id;
this.type = type;
this.source = source;
this.message = message;
this.data = data;
this.userAgent = userAgent;
this.timestamp = timestamp;
this.target = target;
this.tags = tags;
}
getId(): string | undefined {
if (!this.id) {
this.id = generateUUID();
}
return this.id;
}
getTimestamp(): number | undefined {
if (!this.timestamp) {
this.timestamp = Date.now();
}
return this.timestamp;
}
toMap(): Record<string, any> {
return {
id: this.getId(),
type: this.type,
source: this.source,
message: this.message,
data: this.data,
user_agent: this.userAgent,
timestamp: this.getTimestamp(),
target: this.target?.toMap(),
tags: this.tags,
};
}
toJson(): string {
return JSON.stringify(this.toMap());
}
static builder(): CollectorBuilder {
return new CollectorBuilder();
}
}
class CollectorBuilder {
private _id?: string;
private _type?: string;
private _source?: string;
private _message?: string;
private _data?: any;
private _userAgent?: string;
private _timestamp?: number;
private _target?: Target;
private _tags?: string[];
constructor() { }
id(id: string): CollectorBuilder {
this._id = id;
return this;
}
type(type: string | CollectorType): CollectorBuilder {
this._type = type?.toString();
return this;
}
source(source: string | CollectorSource): CollectorBuilder {
this._source = source?.toString();
return this;
}
message(message: string): CollectorBuilder {
this._message = message;
return this;
}
data(data: any): CollectorBuilder {
this._data = data;
return this;
}
userAgent(userAgent: string): CollectorBuilder {
this._userAgent = userAgent;
return this;
}
timestamp(timestamp: number): CollectorBuilder {
this._timestamp = timestamp;
return this;
}
target(target: Target): CollectorBuilder {
this._target = target;
return this;
}
tags(tags: string[]): CollectorBuilder {
this._tags = tags;
return this;
}
build(): Collector {
return new Collector({
id: this._id,
type: this._type,
source: this._source,
message: this._message,
data: this._data,
userAgent: this._userAgent,
timestamp: this._timestamp,
target: this._target,
tags: this._tags,
});
}
}
class CollectorResponse {
message?: string;
id?: string;
constructor({ message, id }: { message?: string; id?: string }) {
this.message = message;
this.id = id;
}
}
class VLogsOptions {
url?: string;
appId?: string;
apiKey?: string;
connectionTimeout?: number;
testConnection?: boolean;
target?: Target;
constructor({
url,
appId,
apiKey,
connectionTimeout,
testConnection,
target,
}: {
url?: string;
appId?: string;
apiKey?: string;
connectionTimeout?: number;
testConnection?: boolean;
target?: Target;
}) {
this.url = url;
this.appId = appId;
this.apiKey = apiKey;
this.connectionTimeout = connectionTimeout;
this.testConnection = testConnection;
this.target = target;
}
static builder(): VLogsOptionsBuilder {
return new VLogsOptionsBuilder();
}
}
class VLogsOptionsBuilder {
private _url?: string;
private _appId?: string;
private _apiKey?: string;
private _connectionTimeout?: number;
private _testConnection?: boolean;
private _target?: Target;
constructor() { }
url(url: string): VLogsOptionsBuilder {
this._url = url;
return this;
}
appId(appId: string): VLogsOptionsBuilder {
this._appId = appId;
return this;
}
apiKey(apiKey: string): VLogsOptionsBuilder {
this._apiKey = apiKey;
return this;
}
connectionTimeout(connectionTimeout: number): VLogsOptionsBuilder {
this._connectionTimeout = connectionTimeout;
return this;
}
testConnection(testConnection: boolean): VLogsOptionsBuilder {
this._testConnection = testConnection;
return this;
}
target(target: Target): VLogsOptionsBuilder {
this._target = target;
return this;
}
telegram(telegram: Telegram): VLogsOptionsBuilder {
if (!this._target) {
this._target = Target.builder().telegram(telegram).build();
} else {
this._target.telegram = telegram;
}
return this;
}
discord(discord: Discord): VLogsOptionsBuilder {
if (!this._target) {
this._target = Target.builder().discord(discord).build();
} else {
this._target.discord = discord;
}
return this;
}
build(): VLogsOptions {
return new VLogsOptions({
url: this._url,
appId: this._appId,
apiKey: this._apiKey,
connectionTimeout: this._connectionTimeout,
testConnection: this._testConnection,
target: this._target,
});
}
}
export {
Collector,
// CollectorBuilder,
CollectorResponse,
Target,
// TargetBuilder,
Telegram,
// TelegramBuilder,
Discord,
// DiscordBuilder,
SDKInfo,
// SDKInfoBuilder,
VLogsOptions,
// VLogsOptionsBuilder,
CollectorSource,
CollectorType,
TelegramParseMode,
}

37
src/service.ts Normal file
View File

@ -0,0 +1,37 @@
import { Collector, CollectorResponse } from './model';
import axios, { AxiosRequestConfig } from 'axios';
class VLogsService {
private url: string;
constructor(baseUrl: string) {
this.url = `${baseUrl}/api/v1/collector`;
}
async post(body: any, headers?: any, timeout?: number): Promise<CollectorResponse> {
const config: AxiosRequestConfig = {
method: 'POST',
url: this.url,
data: body,
headers: headers,
timeout: timeout ? timeout * 1000 : undefined,
};
const response = await axios(config);
if (
response.status === 200 ||
response.status === 201 ||
response.status === 202
) {
return await response.data;
} else {
throw new Error(
`Failed to post data to vlogs server with status code: ${response.status} and message: ${response.statusText}`
);
}
}
}
export { VLogsService };

35
src/util.ts Normal file
View File

@ -0,0 +1,35 @@
import { v4 as uuidv4 } from 'uuid';
export const getSystemHostname = () => {
let name = 'localhost';
if (typeof window !== 'undefined') {
name = window.location.hostname;
}
// @ts-ignore
if (typeof process !== 'undefined') {
// @ts-ignore
name = process.env.HOSTNAME;
}
return name;
}
export const getSystemUsername = () => {
let name = 'unknown';
if (typeof window !== 'undefined') {
name = window.navigator.userAgent;
}
// @ts-ignore
if (typeof process !== 'undefined') {
// @ts-ignore
name = process.env.USER;
}
return name;
}
export const generateUUID = () => {
return uuidv4();
}

86
src/vlgos.ts Normal file
View File

@ -0,0 +1,86 @@
import { Collector, CollectorResponse, SDKInfo, Target, VLogsOptions } from "./model";
import { VLogsService } from "./service";
import { getSystemHostname, getSystemUsername } from "./util";
export class VLogs {
private static readonly _logger = console;
private static readonly NAME = 'vlogs';
private static readonly VERSION = '1.0.0';
private static readonly VERSION_CODE = '1';
private static readonly DEFAULT_VLOGS_URL = 'https://vlogs-sg1.onrender.com';
private static readonly APP_ID_HEADER_PREFIX = 'x-app-id';
private static readonly API_KEY_HEADER_PREFIX = 'x-api-key';
private static readonly DEFAULT_CONNECT_TIMEOUT = 60; // seconds
private _options!: VLogsOptions;
private _service!: VLogsService;
constructor(options: VLogsOptions) {
if (!options.appId || !options.apiKey) {
throw new Error('AppID and ApiKey are required');
}
// Set default options
this._options = options;
this._options.url ??= VLogs.DEFAULT_VLOGS_URL;
// Initialize service
this._service = new VLogsService(this._options.url);
VLogs._logger.log(`VLogs: Initialized AppID: ${this._options.appId} | SDK Version: ${VLogs.VERSION}-${VLogs.VERSION_CODE}`);
}
async collect(request: Collector): Promise<CollectorResponse> {
VLogs._logger.info(`VLogs: Collecting logs for ${request.getId()}`);
const headers: Record<string, string> = {
[VLogs.APP_ID_HEADER_PREFIX]: this._options.appId!,
[VLogs.API_KEY_HEADER_PREFIX]: this._options.apiKey!,
'Content-Type': 'application/json',
};
const hostname = getSystemHostname();
const sender = getSystemUsername();
const sdkInfo = SDKInfo.builder()
.hostname(hostname)
.sender(sender)
.name(VLogs.NAME)
.version(VLogs.VERSION)
.versionCode(VLogs.VERSION_CODE)
.build();
if (!request.target) {
if (this._options.target) {
request.target = this._options.target;
} else {
request.target = Target.builder().build();
}
} else {
if (this._options.target) {
request.target!.merge(this._options.target);
}
}
// Set SDK info to request
request.target!.sdkInfo = sdkInfo;
// Append user agent to request
request.userAgent ??= `vlogs-ts-sdk/${VLogs.VERSION}-${VLogs.VERSION_CODE} (${hostname})`;
const response = await this._service.post(request.toMap(), headers, this._options.connectionTimeout ?? VLogs.DEFAULT_CONNECT_TIMEOUT);
return response;
}
static create(options: VLogsOptions): VLogs {
return new VLogs(options);
}
static createWith(appId: string, apiKey: string): VLogs {
return VLogs.create(
VLogsOptions.builder()
.apiKey(apiKey)
.appId(appId)
.build()
);
}
}

59
tests/vlogs.test.ts Normal file
View File

@ -0,0 +1,59 @@
import { Collector, CollectorSource, CollectorType, VLogs, VLogsOptions } from '../src';
const APP_ID = '72bd14c306a91fa8a590330e3898ddcc';
const API_KEY = 'vlogs_gX9WwSdKatMNdpUClLU0IfCx575tvdoeQ'
const sdk = VLogs.create(
VLogsOptions.builder()
.appId(APP_ID)
.apiKey(API_KEY)
// .target(Target.withTelegram("xxx"))
.build()
)
test('VLogs sdk should be defined', () => {
expect(sdk).toBeDefined();
});
test('VLogs sdk should be able to collect logs', async () => {
const request = Collector.builder()
.message('Hello from vlogs-ts-sdk')
.type(CollectorType.Log)
.source(CollectorSource.Web)
.build();
const response = await sdk.collect(request);
console.log("Request: ", request);
console.log("Response: ", response);
expect(request.getId()).toBeDefined();
expect(request.getId()).not.toBeNull();
expect(request.getId()).toEqual(response.id);
expect(response).toBeDefined();
expect(response.id).toBeDefined();
expect(response.id).not.toBeNull();
expect(response.id).not.toBe('');
})
test('VLogs sdk should be able to collect logs with target', async () => {
const request = Collector.builder()
.message('Hello from vlogs-ts-sdk')
.type(CollectorType.Log)
.source(CollectorSource.Web)
// .target(Target.withTelegram("xxx"))
.build();
const response = await sdk.collect(request);
console.log("Request: ", request);
console.log("Response: ", response);
expect(request.getId()).toBeDefined();
expect(request.getId()).not.toBeNull();
expect(request.getId()).toEqual(response.id);
expect(response).toBeDefined();
expect(response.id).toBeDefined();
expect(response.id).not.toBeNull();
expect(response.id).not.toBe('');
})

12
tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"types": ["jest"]
},
"include": ["src/**/*.ts"]
}