Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"useNx": false,
"packages": ["libs/*", "apps/*"],
"version": "13.0.0",
"version": "14.0.0-pr76.5",
"command": {
"version": {
"allowBranch": "*",
Expand Down
6 changes: 3 additions & 3 deletions libs/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shiftcode/ngx-components",
"version": "13.0.0",
"version": "14.0.0-pr76.5",
"repository": "https://github.com/shiftcode/sc-ng-commons-public",
"license": "MIT",
"author": "shiftcode GmbH <team@shiftcode.ch>",
Expand All @@ -20,8 +20,8 @@
"@angular/core": "^21.0.3",
"@angular/forms": "^21.0.3",
"@angular/router": "^21.0.3",
"@shiftcode/logger": "^3.0.0",
"@shiftcode/ngx-core": "^13.0.0 || ^13.0.0-pr63",
"@shiftcode/logger": "^4.0.0-pr250",
"@shiftcode/ngx-core": "^14.0.0 || ^14.0.0-pr76",
"rxjs": "^6.5.3 || ^7.4.0"
}
}
4 changes: 2 additions & 2 deletions libs/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shiftcode/ngx-core",
"version": "13.0.0",
"version": "14.0.0-pr76.5",
"repository": "https://github.com/shiftcode/sc-ng-commons-public",
"license": "MIT",
"author": "shiftcode GmbH <team@shiftcode.ch>",
Expand All @@ -18,7 +18,7 @@
"@angular/core": "^21.0.3",
"@angular/platform-browser": "^21.0.3",
"@angular/router": "^21.0.3",
"@shiftcode/logger": "^3.0.0",
"@shiftcode/logger": "^4.0.0-pr250",
"@shiftcode/utilities": "^4.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
/* eslint-disable no-console */
import { isPlatformServer } from '@angular/common'
import { inject, Injectable, PLATFORM_ID } from '@angular/core'
import { inject, Injectable, InjectionToken, PLATFORM_ID } from '@angular/core'
import { LogLevel, LogTransport } from '@shiftcode/logger'

import { leadingZero } from '../helper/leading-zero.function'
import { CONSOLE_LOG_TRANSPORT_CONFIG } from './console-log-transport-config.injection-token'
import { loggingTimeFormat } from '../helper/logging-time-format.const'

export interface BrowserConsoleLogTransportConfig {
logLevel: LogLevel
}

export const BROWSER_CONSOLE_LOG_TRANSPORT_CONFIG = new InjectionToken<BrowserConsoleLogTransportConfig>(
'BROWSER_CONSOLE_LOG_TRANSPORT_CONFIG',
{ factory: () => ({ logLevel: LogLevel.DEBUG }) },
)

@Injectable({ providedIn: 'root' })
export class ConsoleLogTransport extends LogTransport {
export class BrowserConsoleLogTransportService extends LogTransport {
constructor() {
super(inject(CONSOLE_LOG_TRANSPORT_CONFIG).logLevel)
super(inject(BROWSER_CONSOLE_LOG_TRANSPORT_CONFIG).logLevel)
if (isPlatformServer(inject(PLATFORM_ID))) {
throw new Error('This log transport is only for client side use - consider using "NodeConsoleLogTransport"')
}
}

log(level: LogLevel, clazzName: string, color: string, timestamp: Date, args: any[]) {
if (this.isLevelEnabled(level)) {
const now = [
leadingZero(2, timestamp.getHours()),
leadingZero(2, timestamp.getMinutes()),
leadingZero(2, timestamp.getSeconds()),
leadingZero(3, timestamp.getMilliseconds()),
].join(':') // 'HH:mm:ss:SSS'
const now = loggingTimeFormat.format(timestamp)

const firstArgument = args.splice(0, 1)[0]

if (typeof firstArgument === 'string') {
Expand All @@ -32,6 +35,7 @@ export class ConsoleLogTransport extends LogTransport {
args.splice(0, 0, `%c${now} - ${clazzName} ::`, `color:${color}`, firstArgument)
}

/* eslint-disable no-console */
switch (level) {
case LogLevel.DEBUG:
console.debug(...args)
Expand All @@ -47,7 +51,10 @@ export class ConsoleLogTransport extends LogTransport {
break
case LogLevel.OFF:
break
default:
return level // exhaustive check
}
/* eslint-enable no-console */
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { LogTransport } from '@shiftcode/logger'

import { LoggerFeature } from '../logger-feature.type'
import { LoggerFeatureKind } from '../logger-feature-kind.enum'
import {
BROWSER_CONSOLE_LOG_TRANSPORT_CONFIG,
BrowserConsoleLogTransportConfig,
BrowserConsoleLogTransportService,
} from './browser-console-log-transport.service'

export function withBrowserConsoleTransport(
browserConsoleLogTransportConfigOrFactory:
| BrowserConsoleLogTransportConfig
| (() => BrowserConsoleLogTransportConfig),
): LoggerFeature {
const configProvider =
typeof browserConsoleLogTransportConfigOrFactory === 'function'
? { provide: BROWSER_CONSOLE_LOG_TRANSPORT_CONFIG, useFactory: browserConsoleLogTransportConfigOrFactory }
: { provide: BROWSER_CONSOLE_LOG_TRANSPORT_CONFIG, useValue: browserConsoleLogTransportConfigOrFactory }

return {
kind: LoggerFeatureKind.TRANSPORT,
providers: [configProvider, { provide: LogTransport, useClass: BrowserConsoleLogTransportService, multi: true }],
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// eslint-disable-next-line max-classes-per-file
import { inject, Injectable } from '@angular/core'
import { ContentType } from '@shiftcode/utilities'
import { CommonHttpHeader } from '@shiftcode/utilities'

import { CLOUD_WATCH_LOG_TRANSPORT_CONFIG_V2 } from './cloud-watch-log-transport-config.injection-token'

export interface LogStream {
logStreamName: string
creationTime: number
lastIngestionTime: number | null
}

export interface LogEvent {
message: string
timestamp: number
}

export interface WriteLogEvents {
logEvents: LogEvent[]
}

export class HttpError extends Error {
override readonly name: string = 'HttpError'

constructor(
readonly statusCode: number,
message: string,
) {
super(`${statusCode} - ${message}`)
}
}

export class HttpApiError extends HttpError {
override readonly name: string = 'HttpApiError'

constructor(
statusCode: number,
readonly errorCode: string,
message: string,
) {
super(statusCode, `${message} (${errorCode})`)
}
}

enum ApiPath {
STREAMS = 'streams',
STREAM_LOGS = 'logs',
}

@Injectable({ providedIn: 'root' })
export class CloudWatchLogApiService {
private readonly apiUrl = inject(CLOUD_WATCH_LOG_TRANSPORT_CONFIG_V2).apiUrl

async createLogStream(logStreamName: string): Promise<void> {
const resp = await fetch(new URL(ApiPath.STREAMS, this.apiUrl), {
method: 'POST',
headers: { [CommonHttpHeader.CONTENT_TYPE]: ContentType.JSON },
body: JSON.stringify({ logStreamName }),
})
await this.handleError(resp)
}

async describeLogStream(logStreamName: string): Promise<LogStream> {
const result = await fetch(new URL(`${ApiPath.STREAMS}/${logStreamName}`, this.apiUrl), {
method: 'GET',
headers: { [CommonHttpHeader.CONTENT_TYPE]: ContentType.JSON },
})
await this.handleError(result)
return (await result.json()) as LogStream
}

async writeLogs(logStreamName: string, logs: LogEvent[]): Promise<void> {
// todo: use beaconApi ?
const resp = await fetch(new URL(`${ApiPath.STREAMS}/${logStreamName}/${ApiPath.STREAM_LOGS}`, this.apiUrl), {
method: 'POST',
headers: { [CommonHttpHeader.CONTENT_TYPE]: ContentType.JSON },
body: JSON.stringify({ logEvents: logs } satisfies WriteLogEvents),
})
await this.handleError(resp)
}

private async handleError(resp: Response): Promise<void> {
if (!resp.ok) {
const errorResponse: object = (await resp.json().catch(() => ({}))) ?? {}
if (
'message' in errorResponse &&
typeof errorResponse.message === 'string' &&
'code' in errorResponse &&
typeof errorResponse.code === 'string'
) {
throw new HttpApiError(resp.status, errorResponse.code, errorResponse.message)
} else {
throw new HttpError(resp.status, 'unknown error')
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ErrorHandler, inject, Injectable, Injector } from '@angular/core'
import { LogLevel } from '@shiftcode/logger'

import { CloudWatchLogServiceV2 } from './cloud-watch-log.service'

/**
* Angular ErrorHandler to send uncaught Errors to AWS CloudWatch Logs
* requires the {@link CloudWatchLogServiceV2}
*/
@Injectable({ providedIn: 'root' })
export class CloudWatchLogErrorHandlerV2 extends ErrorHandler {
private readonly injector = inject(Injector)

override handleError(error: any): void {
// prevent cyclic dependencies (eg. when CLOUD_WATCH_LOG_TRANSPORT_CONFIG needs config from httpClient request)
const cws = this.injector.get(CloudWatchLogServiceV2)
cws.addMessage(LogLevel.ERROR, 'BrowserJsException', new Date(), [error])

// call super.handleError to print error the angular way to the console
super.handleError(error)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { InjectionToken } from '@angular/core'
import { LogLevel } from '@shiftcode/logger'

export interface CloudWatchLogTransportConfigV2 {
logLevel: LogLevel
apiUrl: string

/** milliseconds until logs are flushed to aws */
flushInterval: number
jsonStringifyReplacer?: (key: string, value: any) => any

/** max number of sub-threshold log events to buffer before dropping oldest. default 100 */
bufferSize?: number
}

export const CLOUD_WATCH_LOG_TRANSPORT_CONFIG_V2 = new InjectionToken<CloudWatchLogTransportConfigV2>(
'CLOUD_WATCH_LOG_TRANSPORT_CONFIG_V2',
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { inject, Injectable } from '@angular/core'
import { LogLevel, LogTransport } from '@shiftcode/logger'

import { CloudWatchLogServiceV2 } from './cloud-watch-log.service'
import { CLOUD_WATCH_LOG_TRANSPORT_CONFIG_V2 } from './cloud-watch-log-transport-config.injection-token'

/**
* The LogTransport implementation using {@link CloudWatchLogServiceV2}.
* Delegates all logging logic to the CloudWatchLogger.
* Requires the {@link CLOUD_WATCH_LOG_TRANSPORT_CONFIG_V2} to be provided.
*/
@Injectable({ providedIn: 'root' })
export class CloudWatchLogTransportServiceV2 extends LogTransport {
private readonly cloudWatchLogger = inject(CloudWatchLogServiceV2)

constructor() {
super(inject(CLOUD_WATCH_LOG_TRANSPORT_CONFIG_V2).logLevel)
}

log(level: LogLevel, clazzName: string, _color: string, timestamp: Date, args: unknown[]): void {
// checking the log level is done in the cloudWatchLogger. important.
this.cloudWatchLogger.addMessage(level, clazzName, timestamp, args)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Push an item to a ring buffer array. If the buffer exceeds maxSize, the oldest entry is removed.
* todo: use from @shiftcode/logger when available
*/
export function pushToRingBuffer<T>(buffer: T[], item: T, maxSize: number): void {
buffer.push(item)
if (buffer.length > maxSize) {
buffer.shift()
}
}
Loading