Skip to content

Commit

Permalink
Merge pull request #5 from CloudPhoenix/refactor
Browse files Browse the repository at this point in the history
Refactor
  • Loading branch information
Dam998 authored Feb 23, 2024
2 parents 3ea4866 + 45008ad commit aec20b0
Show file tree
Hide file tree
Showing 11 changed files with 2,137 additions and 118 deletions.
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
setupFiles: ["dotenv/config"],
}
93 changes: 93 additions & 0 deletions lib/LogFlake.ts
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))
})
}
}
}
110 changes: 2 additions & 108 deletions lib/index.ts
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"
21 changes: 21 additions & 0 deletions lib/init.ts
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
}
26 changes: 26 additions & 0 deletions lib/types.ts
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
}
7 changes: 7 additions & 0 deletions lib/utils.ts
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()
}
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"prepare": "npm run build",
"lint": "eslint \"{**/*,*}.{js,ts}\"",
"prettier": "prettier --write \"{lib,tests,example/src}/**/*.{js,ts}\"",
"prepublishOnly": "npm run prettier && npm run lint"
"prepublishOnly": "npm run prettier && npm run lint",
"test": "jest"
},
"author": "Cloud Phoenix",
"license": "MIT",
Expand All @@ -29,13 +30,17 @@
"whatwg-fetch": "3.6.19"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "20.10.3",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"dotenv": "^16.4.5",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"ts-jest": "^29.1.2",
"typescript": "5.3.2"
},
"publishConfig": {
Expand Down
20 changes: 20 additions & 0 deletions tests/init.test.ts
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)
})
})
Loading

0 comments on commit aec20b0

Please sign in to comment.