@wireio/shared 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +424 -0
  2. package/package.json +5 -1
  3. package/src/guards/applyTypeGuardExtras.ts +0 -9
  4. package/src/guards/array.ts +0 -5
  5. package/src/guards/assert.ts +0 -83
  6. package/src/guards/class.ts +0 -56
  7. package/src/guards/guard-tools.ts +0 -78
  8. package/src/guards/index.ts +0 -7
  9. package/src/guards/primitive.ts +0 -77
  10. package/src/guards/types.ts +0 -15
  11. package/src/helpers/Deferred.ts +0 -149
  12. package/src/helpers/index.ts +0 -1
  13. package/src/index.ts +0 -3
  14. package/src/logging/Appender.ts +0 -5
  15. package/src/logging/Formatter.ts +0 -8
  16. package/src/logging/InternalLogger.ts +0 -6
  17. package/src/logging/Level.ts +0 -19
  18. package/src/logging/LogRecord.ts +0 -17
  19. package/src/logging/Logger.ts +0 -222
  20. package/src/logging/LoggingManager.ts +0 -306
  21. package/src/logging/appenders/ConsoleAppender.ts +0 -110
  22. package/src/logging/appenders/DebugAppender.ts +0 -254
  23. package/src/logging/appenders/FileAppender.ts +0 -281
  24. package/src/logging/appenders/aws-firehose/AWSFirehoseAppender.ts +0 -176
  25. package/src/logging/appenders/aws-firehose/AWSFirehoseCredentialManager.ts +0 -132
  26. package/src/logging/appenders/index.ts +0 -3
  27. package/src/logging/context/LogContext.ts +0 -74
  28. package/src/logging/context/LogContextContainer.ts +0 -28
  29. package/src/logging/context/LogContextProvider.ts +0 -43
  30. package/src/logging/context/index.ts +0 -3
  31. package/src/logging/context/providers/node/NodeContextProvider.ts +0 -45
  32. package/src/logging/context/providers/node/index.ts +0 -3
  33. package/src/logging/examples/example-debug-appender.ts +0 -19
  34. package/src/logging/examples/example-file-appender.ts +0 -19
  35. package/src/logging/getLogger.ts +0 -9
  36. package/src/logging/index-node.ts +0 -2
  37. package/src/logging/index.ts +0 -11
  38. package/src/logging/util.ts +0 -6
  39. package/tsconfig.json +0 -9
  40. package/tsconfig.tsbuildinfo +0 -1
@@ -1,281 +0,0 @@
1
- import { assign, defaults, padStart } from "lodash"
2
- import * as Path from "path"
3
- import * as Fs from "fs"
4
- import type { LogRecord } from "../LogRecord"
5
- import type { Appender } from "../Appender"
6
- import { Option, Future } from "prelude-ts"
7
- import { Buffer } from "buffer"
8
- import Debug from "debug"
9
- import Bluebird from "bluebird"
10
-
11
- const FsAsync = Fs.promises
12
-
13
- const debug = Debug("3fv:logger:FileAppender")
14
- const osTempDir = process.env.TMP ?? process.env.TEMP ?? "/tmp"
15
- const tempDir = Fs.mkdtempSync(Path.join(osTempDir, "logger-proxy-example")) //
16
- const getDefaultConfig = (): FileAppenderConfig => ({
17
- filename: Path.join(tempDir, "app.log"),
18
- prettyPrint: process.env.NODE_ENV !== "production",
19
- maxSize: -1,
20
- maxFiles: 5,
21
- enableRolling: false,
22
- sync: false
23
- })
24
-
25
- function applyConfigDefaults(options: FileAppenderOptions): FileAppenderConfig {
26
- return defaults(options, getDefaultConfig())
27
- }
28
-
29
- export interface FileAppenderConfig<Record extends LogRecord = any> {
30
- filename: string
31
-
32
- /**
33
- * For rolling
34
- */
35
- maxFiles: number
36
-
37
- /**
38
- * For rolling
39
- */
40
- maxSize: number
41
-
42
- enableRolling: boolean
43
- prettyPrint: boolean
44
- sync: boolean
45
- }
46
-
47
- export type FileAppenderOptions<Record extends LogRecord = any> = Partial<
48
- FileAppenderConfig<Record>
49
- >
50
-
51
- export class FileAppender<Record extends LogRecord>
52
- implements Appender<Record>
53
- {
54
- readonly config: FileAppenderConfig<Record>
55
-
56
- private readonly state: {
57
- filename: string
58
- file: number
59
- flushing: boolean
60
- queue: Array<Buffer>
61
- ready: boolean
62
- error?: Error
63
- currentSize: number
64
- archivedFilenames: string[]
65
- } = {
66
- archivedFilenames: [],
67
- filename: undefined,
68
- file: undefined,
69
- ready: false,
70
- currentSize: 0,
71
- flushing: false,
72
- queue: []
73
- }
74
-
75
- get enableRolling() {
76
- return this.config.enableRolling
77
- }
78
-
79
- isReady() {
80
- return this.state.ready
81
- }
82
-
83
- /**
84
- * Initialize and setup the appender
85
- *
86
- * @returns {FileAppender<Record>}
87
- */
88
- setup(): FileAppender<Record> {
89
- const { state } = this
90
- if (!!state.ready) {
91
- return this
92
- }
93
-
94
- try {
95
- const { filename } = this
96
- const file = Fs.openSync(filename, "a")
97
-
98
- assign(state, {
99
- filename,
100
- file,
101
- ready: true
102
- })
103
-
104
- return this
105
- } catch (err) {
106
- assign(state, {
107
- error: err,
108
- file: undefined,
109
- ready: false
110
- })
111
- throw err
112
- }
113
- }
114
-
115
- /**
116
- * Only exposed for the sake of tests & examples
117
- * DO NOT USE
118
- *
119
- * This should all be async, but i was in a rush :(
120
- *
121
- * @returns
122
- */
123
- rollFile() {
124
- const { state, config } = this
125
- let { file, filename, archivedFilenames } = state
126
- if (!this.isReady() || !file) {
127
- debug(`Can not roll file before ready`)
128
- return
129
- }
130
-
131
- if (!this.enableRolling) {
132
- return
133
- }
134
-
135
- const fileCount = archivedFilenames.length + 1
136
- if (config.maxFiles < 1 || fileCount < config.maxFiles) {
137
- archivedFilenames.push(
138
- filename + "." + padStart(fileCount.toString(10), 4, "0")
139
- )
140
- }
141
- Fs.fdatasyncSync(file)
142
- Fs.closeSync(file)
143
- file = state.file = undefined
144
- const filenames = [filename, ...archivedFilenames]
145
-
146
- let dest: string = filenames.pop()
147
- while (filenames.length) {
148
- let src = filenames.pop()
149
- if (!Fs.existsSync(src)) {
150
- debug(`src does not exist, skipping ${src}`)
151
- } else {
152
- debug(`Moving src (${src}) to dest (${dest})`)
153
- if (Fs.existsSync(dest)) {
154
- Fs.unlinkSync(dest)
155
- }
156
- Fs.renameSync(src, dest)
157
- // if (src === filename) {
158
- // Fs.copyFileSync(src, dest)
159
- // Fs.truncateSync(src, 0)
160
- // } else {
161
- // Fs.renameSync(src, dest)
162
- // }
163
- }
164
- dest = src
165
- }
166
-
167
- assign(state, {
168
- currentSize: 0,
169
- file: Fs.openSync(filename, "a")
170
- })
171
- }
172
-
173
- /**
174
- * Close the handler
175
- *
176
- * @returns {Promise<void>}
177
- */
178
- close() {
179
- return this.flush().onComplete(() =>
180
- Option.of(this.state.file).map(Fs.closeSync)
181
- ).toPromise()
182
- }
183
-
184
- get queue() {
185
- return this.state.queue
186
- }
187
-
188
- get file() {
189
- return this.state.file
190
- }
191
-
192
- get flushing() {
193
- return this.state.flushing
194
- }
195
-
196
- get filename() {
197
- return (this.state.filename = this.state.filename ?? this.config.filename)
198
- }
199
-
200
- /**
201
- * Appends the log queue records to the file
202
- */
203
- private flush() {
204
- const { state } = this
205
- return Future.do(async () => {
206
- state.flushing = true
207
- try {
208
- const { file } = this
209
- if (!file) {
210
- throw Error(`${this.filename} was not opened properly`)
211
- }
212
- while (this.queue.length) {
213
- const buf: Buffer = this.queue.shift()
214
-
215
- await Bluebird.fromCallback((cb) =>
216
- Fs.appendFile(file, buf, "utf-8", cb)
217
- )
218
-
219
- state.currentSize += buf.byteLength
220
- }
221
-
222
- const { maxSize } = this.config
223
- if (maxSize > 0 && state.currentSize >= maxSize) {
224
- this.rollFile()
225
- }
226
-
227
- //await Bluebird.fromCallback((cb) => Fs.fdatasync(this.file, cb))
228
- } catch (err) {
229
- console.error(`Failed to append file ${this.filename}`, err)
230
- } finally {
231
- this.state.flushing = false
232
- if (this.queue.length) {
233
- queueMicrotask(() => this.flush())
234
- }
235
- }
236
- })
237
- }
238
-
239
- /**
240
- * Handle log records, transform, push to ES
241
- *
242
- * @param record
243
- */
244
- append(record: Record): void {
245
- try {
246
- const { queue, filename } = this.state
247
- const count = queue.length
248
- if (count > 999) {
249
- debug(
250
- `Too many log records (${count}) are in the queue without the file (${filename}) opening, skipping %O`,
251
- record
252
- )
253
- return
254
- }
255
-
256
- const data = this.config.prettyPrint
257
- ? JSON.stringify(record, null, 2)
258
- : JSON.stringify(record)
259
- queue.push(Buffer.from(data + "\n", "utf-8"))
260
- this.flush()
261
- } catch (err) {
262
- console.warn(`Failed to synchronize `, err)
263
- }
264
- }
265
-
266
- /**
267
- *
268
- * @param {Partial<FileAppenderOptions<Record>>} options
269
- */
270
- constructor(options: Partial<FileAppenderOptions<Record>> = {}) {
271
- this.config = applyConfigDefaults(options)
272
-
273
- assign(this.state, {
274
- filename: this.config.filename
275
- })
276
-
277
- this.setup()
278
- debug(`File appender is ready, logging to ${this.state.filename}`)
279
- // return this
280
- }
281
- }
@@ -1,176 +0,0 @@
1
- /* eslint-disable typescript/no-explicit-any */
2
- import { isString } from '../../../guards/primitive.js'
3
- import { FirehoseClient, PutRecordBatchCommand, PutRecordBatchInput } from "@aws-sdk/client-firehose"
4
- import {
5
- Credentials as IAWSTemporaryCredentials,
6
- ExpiredTokenException,
7
- } from "@aws-sdk/client-sts"
8
- import { Level } from "../../Level.js"
9
-
10
- import AWSFirehoseCredentialManager from "./AWSFirehoseCredentialManager.js"
11
- import { getInternalLogger } from "../../InternalLogger.js"
12
- import { getValue, guard } from "../../../guards/index.js"
13
- import { pick } from "lodash"
14
- import { Deferred } from "../../../helpers/index.js"
15
- import { LogRecord } from "../../LogRecord.js"
16
- import { Appender } from "../../Appender.js"
17
-
18
- const LogQueueMaxRecords = 10000
19
-
20
- const log = getInternalLogger(import.meta.filename)
21
-
22
-
23
- export class AWSFirehoseAppender<Record extends LogRecord = LogRecord>
24
- implements Appender<Record> {
25
- private pendingRecords = Array<LogRecord>()
26
- private flushDeferred: Deferred<void>
27
- private flushIntervalMs = 1000;
28
-
29
- constructor(public readonly credentialManager: AWSFirehoseCredentialManager = new AWSFirehoseCredentialManager()) {
30
- credentialManager.on("received", () => {
31
- log.info("Credentials received, initializing firehose client")
32
- if (!this.pendingRecords.length || !!this.flushDeferred) {
33
- log.debug("Nothing to flush upon credential receipt OR flush already in progress")
34
- return
35
- }
36
- this.flush()
37
- .catch((e: Error) => {
38
- log.error(`Error flushing logs after credential receipt: ${e.message}`, e)
39
- })
40
- })
41
- }
42
- append(record: Record): void {
43
- const
44
- pendingCount = this.pendingRecords.length + 1,
45
- removeCount = LogQueueMaxRecords - pendingCount
46
-
47
- if (removeCount < 0) {
48
- this.pendingRecords.splice(0, Math.abs(removeCount))
49
- }
50
-
51
- this.pendingRecords.push({
52
- ...record,
53
- app: "web",
54
- timestamp: Date.now(),
55
- env: typeof process === "undefined" ? "WEB" : process.env.WIRE_ENV ?? "SHARED",
56
- url: typeof window === "undefined" ? "local://node" : window.location.href,
57
- })
58
-
59
- this.flush()
60
- }
61
-
62
- /**
63
- * Create the firehose client and return
64
- *
65
- * @param {IAWSTemporaryCredentials} credentials
66
- * @param {boolean} flush
67
- * @returns {Firehose}
68
- */
69
- private getFirehose(): FirehoseClient {
70
- const creds = this.credentialManager.getCredentials()
71
-
72
- if (!creds) {
73
- log.warn("No credentials available for flush")
74
- return null
75
- }
76
-
77
- const { region, streamName } = creds
78
-
79
- if (!region || !streamName) {
80
- throw new Error("Region or stream name not available")
81
- }
82
-
83
- const client = new FirehoseClient({
84
- region: creds.region,
85
- credentials: pick(creds, "accessKeyId", "secretAccessKey", "sessionToken"),
86
- })
87
-
88
- return client
89
- }
90
-
91
- /**
92
- * Flush pending records
93
- *
94
- * @returns {Promise<void>}
95
- */
96
- private async flush(): Promise<void> {
97
- if (!!this.flushDeferred) {
98
- log.debug("Flush already in progress, skipping")
99
- return
100
- }
101
-
102
- if (!this.pendingRecords.length) {
103
- log.debug("No pending records to flush")
104
- return
105
- }
106
- const creds = this.credentialManager.getCredentials()
107
- if (!creds) {
108
- log.warn("No credentials available for flush")
109
- return
110
- }
111
- const { streamName } = creds,
112
- client = this.getFirehose()
113
- if (!client) {
114
- log.warn("No firehose client available, skipping flush")
115
- return
116
- }
117
-
118
- const deferred = this.flushDeferred = new Deferred<void>()
119
- try {
120
- while (this.pendingRecords.length) {
121
- const
122
- chunkSize = Math.min(this.pendingRecords.length, 10),
123
- records = this.pendingRecords.splice(0, chunkSize),
124
- recordJsons = records.map(record => JSON.stringify(record))
125
-
126
- // log.info("Record JSONs", recordJsons)
127
-
128
- const
129
- textEncoder = new TextEncoder(),
130
- batch: PutRecordBatchInput["Records"] = recordJsons.map(jsonStr => ({
131
- Data: textEncoder.encode(jsonStr)
132
- }))
133
-
134
- log.info(`Pushing ${records.length} records to firehose`)
135
-
136
- const cmd = new PutRecordBatchCommand({
137
- DeliveryStreamName: streamName,
138
- Records: batch,
139
- })
140
-
141
- const resp = await client.send(cmd)
142
- const failed = resp.FailedPutCount || 0
143
- if (failed) {
144
- const rr = resp.RequestResponses || []
145
- for (let i = 0; i < rr.length; i++) {
146
- if (rr[i].ErrorCode) {
147
- log.warn(`Record failed: ${rr[i].ErrorCode} ${rr[i].ErrorMessage}`)
148
- //this.pendingRecords.unshift(records[i])
149
- }
150
- }
151
- log.info(`PutRecordBatch failed=${failed}; requeued failed records`)
152
- } else {
153
- log.info(`PutRecordBatch ok count=${records.length}`)
154
- }
155
-
156
- }
157
- } catch (err) {
158
- log.error("Failed to push logs", err)
159
- if (err.code === ExpiredTokenException) {
160
- this.credentialManager.forceUpdateCredentials()
161
- }
162
- deferred.reject(err)
163
- } finally {
164
- this.flushDeferred = null
165
-
166
- // if (this.pendingRecords.length)
167
- // this.flush()
168
- }
169
-
170
- return deferred.promise
171
- }
172
-
173
-
174
- }
175
-
176
- export default AWSFirehoseAppender
@@ -1,132 +0,0 @@
1
- import { FirehoseClient, PutRecordBatchCommand } from "@aws-sdk/client-firehose"
2
- import { Deferred } from "../../../helpers/index.js"
3
- import { getInternalLogger } from "../../InternalLogger.js"
4
- import EventEmitter3 from "eventemitter3"
5
-
6
- const API_BASE = "https://log-ingest.shared.wire-dev.com"
7
- const CRED_BUFFER_PADDING_MS = 3 * 60 * 1000 // 3 minutes
8
-
9
- const log = getInternalLogger(import.meta.filename)
10
-
11
- export interface AWSCredentials {
12
- accessKeyId: string
13
- secretAccessKey: string
14
- sessionToken: string
15
- streamName: string
16
- region: string
17
- expiration: string
18
- }
19
-
20
- export interface AWSFirehoseCredentialManagerEventMap {
21
- received: (creds: AWSCredentials) => void
22
- expired: () => void
23
- }
24
-
25
- export class AWSFirehoseCredentialManager extends EventEmitter3<AWSFirehoseCredentialManagerEventMap> {
26
-
27
- private refreshTimer: ReturnType<typeof setTimeout> = null;
28
- private loadDeferred: Deferred<AWSCredentials> = null;
29
-
30
- forceUpdateCredentials(): void {
31
- if (this.loadDeferred && !this.loadDeferred.isSettled()) {
32
- this.loadDeferred.reject(new Error("force update"))
33
- }
34
-
35
- this.loadDeferred = null
36
- this.scheduleRefresh(1)
37
- }
38
-
39
- getCredentials(): AWSCredentials {
40
- if (this.loadDeferred && this.loadDeferred.isFulfilled()) {
41
- const creds = this.loadDeferred.getResult(),
42
- expMs = Date.parse(creds.expiration)
43
-
44
- if (expMs <= Date.now()) {
45
- log.info("Credentials expired")
46
- this.loadDeferred = null
47
- this.scheduleRefresh(1)
48
- this.emit("expired")
49
- return null
50
- }
51
- return creds
52
- }
53
-
54
- this.scheduleRefresh(1)
55
- return null
56
- }
57
-
58
- hasCredentials(): boolean {
59
- return this.getCredentials() != null
60
- }
61
-
62
- private async load(): Promise<AWSCredentials> {
63
- if (this.loadDeferred && !this.loadDeferred.isRejected())
64
- return this.loadDeferred.promise
65
-
66
- const deferred = this.loadDeferred = new Deferred<AWSCredentials>()
67
- try {
68
- const r = await fetch(API_BASE + "/creds", {
69
- method: "GET",
70
- credentials: "include",
71
- })
72
- if (!r.ok) throw new Error(`creds http ${r.status}`)
73
- const creds: AWSCredentials = await r.json()
74
-
75
-
76
- this.scheduleRefresh(creds)
77
- log.info(`Got creds, expire=${creds.expiration}, stream=${creds.streamName}, region=${creds.region}`)
78
- deferred.resolve(creds)
79
- this.emit("received", creds)
80
- return deferred.promise
81
- } catch (e) {
82
- deferred.reject(e)
83
- this.loadDeferred = null
84
- throw e
85
- }
86
- }
87
- scheduleRefresh(credsOrMillis: AWSCredentials | number): void {
88
- if (this.refreshTimer) {
89
- clearTimeout(this.refreshTimer)
90
- this.refreshTimer = null
91
- }
92
- if (!credsOrMillis)
93
- return
94
-
95
- let refreshInMs: number
96
- if (typeof credsOrMillis === "number") {
97
- refreshInMs = credsOrMillis
98
- } else {
99
- const
100
- creds = credsOrMillis,
101
- expMs = Date.parse(creds.expiration)
102
-
103
- refreshInMs = expMs - Date.now() - CRED_BUFFER_PADDING_MS
104
-
105
- }
106
-
107
- refreshInMs = Math.max(100, refreshInMs)
108
-
109
- this.refreshTimer = setTimeout(() => {
110
- this.loadDeferred = null
111
- this.load().catch((e: Error) => log.error(`refresh failed: ${e.message}`, e))
112
- }, refreshInMs)
113
- }
114
-
115
- getStreamName(): string {
116
- return this.getCredentials()?.streamName
117
- }
118
-
119
- getRegion(): string {
120
- return this.getCredentials()?.region
121
- }
122
-
123
- destroy(): void {
124
- this.scheduleRefresh(null)
125
- if (!this.loadDeferred.isSettled())
126
- this.loadDeferred.reject(new Error("destroyed"))
127
- this.loadDeferred = null
128
- }
129
- }
130
-
131
-
132
- export default AWSFirehoseCredentialManager
@@ -1,3 +0,0 @@
1
- export * from "./aws-firehose/AWSFirehoseAppender.js"
2
- export * from "./ConsoleAppender.js"
3
- export * from "./DebugAppender.js"
@@ -1,74 +0,0 @@
1
- import { Appender } from "../Appender.js"
2
- import { Level } from "../Level.js"
3
- import { LogContextProvider } from "./LogContextProvider.js"
4
- import { LogContextContainer } from "./LogContextContainer.js"
5
-
6
-
7
-
8
-
9
-
10
-
11
- export class LogContext {
12
- /**
13
- * Threshold override level
14
- *
15
- * @type {Level}
16
- */
17
- readonly thresholdLevel: Level
18
-
19
- /**
20
- * If specified, only categories that match will
21
- * use this context
22
- *
23
- * @type {RegExp}
24
- */
25
- readonly pattern: RegExp
26
-
27
- /**
28
- * Exclusive, only use this config in context. Defaults to `false`
29
- *
30
- * @type {boolean}
31
- */
32
- readonly exclusive: RegExp
33
-
34
- /**
35
- * Use a logger context within
36
- *
37
- * @param {() => Promise<T>} fn
38
- * @returns {Promise<T>}
39
- */
40
- use<T = unknown>(fn: () => Promise<T>) {
41
- return LogContextContainer.runInContext(this, fn)
42
- }
43
-
44
- /**
45
- * Creates a new logger context
46
- *
47
- * @param {Appender[]} appenders - additional appenders for the context
48
- * @param {LoggerContextOptions} options -
49
- */
50
- protected constructor(
51
- readonly appenders: Appender[],
52
- options: LoggerContextOptions
53
- ) {
54
- Object.assign(this, {
55
- exclusive: false,
56
- thresholdLevel: null,
57
- pattern: null,
58
- ...options
59
- })
60
- }
61
-
62
- /**
63
- * Create a new logger context
64
- *
65
- * @param {Appender[]} appenders
66
- * @param {LoggerContextOptions} options
67
- * @returns {LogContext}
68
- */
69
- static with(appenders: Appender[], options: LoggerContextOptions = {}) {
70
- return new LogContext(appenders, options)
71
- }
72
- }
73
-
74
- export type LoggerContextOptions = Partial<Pick<LogContext, "pattern" | "exclusive">>
@@ -1,28 +0,0 @@
1
- import { LogContext } from "./LogContext.js"
2
- import { DefaultLogContextProvider, LogContextProvider } from "./LogContextProvider.js"
3
-
4
- const state = {
5
- provider: DefaultLogContextProvider as LogContextProvider
6
- }
7
-
8
-
9
- export namespace LogContextContainer {
10
-
11
- export function setProvider(provider: LogContextProvider) {
12
- state.provider = provider ?? DefaultLogContextProvider
13
- }
14
-
15
- export function getProvider():LogContextProvider {
16
- return state.provider ?? DefaultLogContextProvider
17
- }
18
-
19
- export function currentContext(): Array<LogContext> {
20
- return getProvider().currentContext?.() ?? []
21
- }
22
-
23
- export function runInContext<T = unknown>(context: LogContext, fn: () => Promise<T>): Promise<T> {
24
- const provider = getProvider()
25
- return provider.runInContext(context,fn)
26
- }
27
-
28
- }