-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from CloudPhoenix/refactor
Refactor
- Loading branch information
Showing
11 changed files
with
2,137 additions
and
118 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
preset: "ts-jest", | ||
testEnvironment: "node", | ||
setupFiles: ["dotenv/config"], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { IBodyLog, IBodyPerformance, LogLevels, Queue } from "./types" | ||
import { getCurrentDateTime, wait } from "./utils" | ||
import { Buffer } from "buffer" | ||
import { compress } from "snappyjs" | ||
import { hostname as getDefaultHostname } from "os-browserify" | ||
|
||
export class LogFlake { | ||
private readonly server: string | ||
private readonly appId: string | null | ||
private readonly hostname: string | undefined | null | ||
private readonly enableCompression: boolean | ||
|
||
constructor(appId: string, server: string | null = null, options?: { hostname?: string; enableCompression?: boolean }) { | ||
if (appId === null || appId.length === 0) { | ||
throw new Error("App ID must not be empty") | ||
} | ||
this.appId = appId | ||
this.server = server || "https://app.logflake.io" | ||
this.hostname = options?.hostname || getDefaultHostname() | ||
this.enableCompression = options?.enableCompression || true | ||
} | ||
|
||
private async post<T>(queue: Queue, bodyObject: T) { | ||
let contentType = "application/json" | ||
let body: BodyInit = JSON.stringify({ ...bodyObject, datetime: getCurrentDateTime() }) | ||
if (this.enableCompression) { | ||
const encoded = Buffer.from(body).toString("base64") | ||
const encodedBuffer = new TextEncoder().encode(encoded).buffer | ||
body = compress(encodedBuffer) | ||
contentType = "application/octet-stream" | ||
} | ||
|
||
const fetchOptions: RequestInit = { | ||
method: "POST", | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": contentType, | ||
}, | ||
keepalive: true, | ||
body, | ||
} | ||
|
||
const retries = 5 | ||
for (let i = 0; i < retries; i++) { | ||
try { | ||
const response = await fetch(`${this.server}/api/ingestion/${this.appId}/${queue}`, fetchOptions) | ||
if (!response.ok) { | ||
console.error(`LogFlake Error: ${response.status} ${response.statusText}`) | ||
continue | ||
} | ||
|
||
return response | ||
} catch (err) { | ||
console.error(`LogFlake Error (retry ${i + 1}/${retries}):`, err) | ||
await wait(500 * (i + 1)) | ||
} | ||
} | ||
} | ||
|
||
public async sendLog(content: string, options?: Omit<IBodyLog, "content">) { | ||
return this.post<IBodyLog>(Queue.LOGS, { | ||
content, | ||
level: LogLevels.DEBUG, | ||
hostname: this.hostname, | ||
...options, | ||
}) | ||
} | ||
|
||
public async sendException<E extends Error>(exception: E, options?: Pick<IBodyLog, "correlation">) { | ||
return this.post<IBodyLog>(Queue.LOGS, { | ||
content: exception.stack ?? exception.message ?? "Unknown error", | ||
level: LogLevels.EXCEPTION, | ||
params: exception, | ||
hostname: this.hostname, | ||
...options, | ||
}) | ||
} | ||
|
||
public async sendPerformance(label: string, duration: number) { | ||
return this.post<IBodyPerformance>(Queue.PERF, { label, duration }) | ||
} | ||
|
||
public measurePerformance(label: string): () => Promise<number> { | ||
const startTime = Date.now() | ||
|
||
return () => { | ||
return new Promise((resolve) => { | ||
const duration = Date.now() - startTime | ||
this.sendPerformance(label, duration).then(() => resolve(duration)) | ||
}) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,110 +1,4 @@ | ||
import "whatwg-fetch" | ||
import { Buffer } from "buffer" | ||
//@ts-ignore | ||
import * as SnappyJS from "snappyjs" | ||
//@ts-ignore | ||
import * as os from "os-browserify" | ||
|
||
export let Logger: LogFlake | ||
|
||
export function Setup(appid: string, hostname: string | null = null, server: string | null = null, enableCompression: boolean = true) { | ||
Logger = new LogFlake(appid, hostname, server, enableCompression) | ||
} | ||
|
||
export const LogLevels = { | ||
DEBUG: 0, | ||
INFO: 1, | ||
WARNING: 2, | ||
ERROR: 3, | ||
FATAL: 4, | ||
EXCEPTION: 5, | ||
} | ||
|
||
export class LogFlake { | ||
private readonly server: string | ||
private readonly appId: string | null | ||
private readonly hostname: string | null | ||
private readonly enableCompression: boolean | ||
|
||
constructor(appid: string, hostname: string | null = null, server: string | null = null, enableCompression: boolean = true) { | ||
if (appid === null || appid.length === 0) { | ||
throw new Error("App ID must not be empty") | ||
} | ||
this.appId = appid | ||
this.server = server || "https://app.logflake.io" | ||
this.hostname = hostname || os.hostname() || null | ||
this.enableCompression = enableCompression | ||
} | ||
|
||
private async post(queue: string, bodyObject: any) { | ||
if (this.appId === null) { | ||
throw new Error("App ID must not be empty. Call Setup first.") | ||
} | ||
|
||
const fetchOptions: any = { | ||
method: "POST", | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/json", | ||
}, | ||
keepalive: true, | ||
body: JSON.stringify(bodyObject), | ||
} | ||
|
||
if (this.enableCompression) { | ||
const encoded = Buffer.from(fetchOptions.body).toString("base64") | ||
const encodedBuffer = new TextEncoder().encode(encoded).buffer | ||
fetchOptions.body = SnappyJS.compress(encodedBuffer) | ||
fetchOptions.headers["Content-Type"] = "application/octet-stream" | ||
} | ||
|
||
const retries = 3 | ||
for (let i = 0; i < retries; i++) { | ||
try { | ||
return await fetch(`${this.server}/api/ingestion/${this.appId}/${queue}`, fetchOptions) | ||
} catch (err) { | ||
console.error(`LogFlake Error (retry ${i + 1}/${retries}):`, err) | ||
} | ||
} | ||
} | ||
|
||
public async sendLog(level: number, content: string, correlation: string | null = null, params: object | undefined = {}) { | ||
return this.post("logs", { | ||
correlation, | ||
params, | ||
level, | ||
content, | ||
hostname: this.hostname, | ||
}) | ||
} | ||
|
||
public async sendException(exception: Error, correlation: string | null = null) { | ||
return this.post("logs", { | ||
correlation, | ||
content: exception.stack, | ||
hostname: this.hostname, | ||
level: LogLevels.EXCEPTION, | ||
}) | ||
} | ||
|
||
public async sendPerformance(label: string, duration: number | null = null) { | ||
if (label === null || duration === null) { | ||
throw new Error("Label and duration must not be empty") | ||
} | ||
return this.post("perf", { label, duration }) | ||
} | ||
|
||
public measurePerformance(label: string) { | ||
if (label === null) { | ||
throw new Error("Label must not be empty") | ||
} | ||
const startTime = new Date() | ||
return { | ||
stop: async () => { | ||
const duration = new Date().getTime() - startTime.getTime() | ||
await this.sendPerformance(label, duration) | ||
return duration | ||
}, | ||
} | ||
} | ||
} | ||
export * from "./LogFlake" | ||
export * from "./init" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { LogFlake } from "./LogFlake" | ||
|
||
export let Logger: LogFlake | null = null | ||
|
||
interface IInitOptions { | ||
hostname?: string | ||
enableCompression?: boolean | ||
} | ||
|
||
export function initialize(appId: string, server?: string, options?: IInitOptions) { | ||
if (Logger !== null) { | ||
throw new Error("LogFlake is already initialized, if you want to initialize a new instance, use the LogFlake class directly.") | ||
} | ||
|
||
Logger = new LogFlake(appId, server, options) | ||
return Logger | ||
} | ||
|
||
export function reset() { | ||
Logger = null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
export enum LogLevels { | ||
DEBUG = 0, | ||
INFO = 1, | ||
WARNING = 2, | ||
ERROR = 3, | ||
FATAL = 4, | ||
EXCEPTION = 5, | ||
} | ||
|
||
export enum Queue { | ||
LOGS = "logs", | ||
PERF = "perf", | ||
} | ||
|
||
export interface IBodyLog { | ||
correlation?: string | ||
params?: object | ||
level: number | ||
content: string | ||
hostname?: string | null | ||
} | ||
|
||
export interface IBodyPerformance { | ||
label: string | ||
duration: number | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const wait = (delay: number) => { | ||
return new Promise((resolve) => setTimeout(resolve, delay)) | ||
} | ||
|
||
export const getCurrentDateTime = () => { | ||
return new Date().toISOString() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { Logger, initialize } from "../lib/init" | ||
|
||
describe("initialize", () => { | ||
expect(process.env.APP_ID).toBeDefined() | ||
const APP_ID = process.env.APP_ID as string | ||
|
||
expect(process.env.SERVER).toBeDefined() | ||
const SERVER = process.env.SERVER as string | ||
|
||
it("should initialize correctly", () => { | ||
const logInstance = initialize(APP_ID, SERVER) | ||
|
||
expect(logInstance).toBeDefined() | ||
// @ts-expect-error | ||
expect(logInstance?.appId).toBe(APP_ID) | ||
// @ts-expect-error | ||
expect(logInstance?.server).toBe(SERVER) | ||
expect(Logger).toBe(logInstance) | ||
}) | ||
}) |
Oops, something went wrong.