Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5170141
feat(logger): added ConsoleJsonLogTransportService
mumenthalers Jan 21, 2026
97694ee
build(release): next version [skip_build]
mumenthalers Jan 23, 2026
0b5cc64
style(spec): use relative import
mumenthalers Jan 23, 2026
95c1eed
feat(logger): added V2 of CloudWatchLog* to be used with new CloudWat…
mumenthalers Jan 23, 2026
02b43b4
refactor(logger): use latest @shiftcode/logger
mumenthalers Jan 23, 2026
6b3404d
build(deps): use latest minor version for angular 21.0
mumenthalers Jan 23, 2026
2586e5c
style(*): format it all
mumenthalers Jan 23, 2026
9dbe493
build(release): next version [skip_build]
actions-user Jan 23, 2026
fd63419
test(logging-time-format): let it succeed locally & ci
mumenthalers Jan 23, 2026
df67a11
build(release): next version [skip_build]
actions-user Jan 23, 2026
c071a6e
refactor(logger): removal of old cloudwatch logger construct
mumenthalers Jan 23, 2026
0a36809
build(release): next version [skip_build]
actions-user Jan 23, 2026
d8dc25f
refactor(logger): use structuredClone before putting to subject
mumenthalers Jan 23, 2026
70b9870
build(release): next version [skip_build]
actions-user Jan 23, 2026
0f0b5d4
docs(logger): describe defaults
mumenthalers Jan 23, 2026
495db73
build(release): next version [skip_build]
actions-user Jan 23, 2026
ae73af1
refactor(package): correct logger version specification
mumenthalers Jan 23, 2026
12ba866
build(release): next version [skip_build]
actions-user Jan 23, 2026
438ae63
refactor(package): correct logger version specification
mumenthalers Jan 23, 2026
8329dda
build(release): next version [skip_build]
actions-user Jan 23, 2026
e980b7b
feat(cloud-watch-logger): use the beacon api
mumenthalers Feb 3, 2026
a90cce5
build(release): next version [skip_build]
actions-user Feb 3, 2026
12aa1be
Revert "feat(cloud-watch-logger): use the beacon api"
mumenthalers Feb 4, 2026
d765e87
feat(cloud-watch-logger): use keepalive option when writing logs
mumenthalers Feb 4, 2026
24e3e7d
build(release): next version [skip_build]
actions-user Feb 4, 2026
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.14",
"command": {
"version": {
"allowBranch": "*",
Expand Down
16 changes: 8 additions & 8 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.14",
"repository": "https://github.com/shiftcode/sc-ng-commons-public",
"license": "MIT",
"author": "shiftcode GmbH <team@shiftcode.ch>",
Expand All @@ -14,14 +14,14 @@
"tslib": "^2.5.0"
},
"peerDependencies": {
"@angular/animations": "^21.0.3",
"@angular/animations": "^21.0.0",
"@angular/cdk": "^21.0.0",
"@angular/common": "^21.0.3",
"@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",
"@angular/common": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0",
"@angular/router": "^21.0.0",
"@shiftcode/logger": "^4.0.0 || ^4.0.0-pr250",
"@shiftcode/ngx-core": "^14.0.0 || ^14.0.0-pr76",
"rxjs": "^6.5.3 || ^7.4.0"
}
}
12 changes: 6 additions & 6 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.14",
"repository": "https://github.com/shiftcode/sc-ng-commons-public",
"license": "MIT",
"author": "shiftcode GmbH <team@shiftcode.ch>",
Expand All @@ -14,11 +14,11 @@
"tslib": "^2.5.0"
},
"peerDependencies": {
"@angular/common": "^21.0.3",
"@angular/core": "^21.0.3",
"@angular/platform-browser": "^21.0.3",
"@angular/router": "^21.0.3",
"@shiftcode/logger": "^3.0.0",
"@angular/common": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"@angular/router": "^21.0.0",
"@shiftcode/logger": "^4.0.0 || ^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,23 @@
import { LogTransport } from '@shiftcode/logger'

import { ValueOrFactory } from '../helper/value-or-factory.type'
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(config: ValueOrFactory<BrowserConsoleLogTransportConfig>): LoggerFeature {
return {
kind: LoggerFeatureKind.TRANSPORT,
providers: [
{
provide: BROWSER_CONSOLE_LOG_TRANSPORT_CONFIG,
...(typeof config === 'function' ? { useFactory: config } : { useValue: config }),
},
{ provide: LogTransport, useClass: BrowserConsoleLogTransportService, multi: true },
],
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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_V2_CONFIG } from './cloud-watch-log-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 CloudWatchLogV2ApiService {
private readonly apiUrl = inject(CLOUD_WATCH_LOG_V2_CONFIG).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> {
// we do not use the sendBeacon API since it will fail with cors preflight and would require ugly workarounds
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),
keepalive: true, // do not abort request on page unload
})
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,28 @@
import { InjectionToken } from '@angular/core'
import { LogLevel } from '@shiftcode/logger'

export interface CloudWatchLogV2Config {
logLevel: LogLevel

/**
* the url of the CloudWatchApi Construct from @shiftcode/cdk-utils
*/
apiUrl: string

/** milliseconds until logs are flushed to aws */
flushInterval: number

/**
* replacer function for JSON.stringify
* @default {@link jsonMapSetStringifyReplacer}
*/
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_V2_CONFIG = new InjectionToken<CloudWatchLogV2Config>('CLOUD_WATCH_LOG_V2_CONFIG')
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { ErrorHandler, inject, Injectable, Injector } from '@angular/core'
import { LogLevel } from '@shiftcode/logger'

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

/**
* Angular ErrorHandler to send uncaught Errors to AWS CloudWatch Logs
* requires the {@link CloudWatchService}
* requires the {@link CloudWatchLogV2Service}
*/
@Injectable({ providedIn: 'root' })
export class CloudWatchErrorHandler extends ErrorHandler {
export class CloudWatchLogV2ErrorHandler 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(CloudWatchService)
const cws = this.injector.get(CloudWatchLogV2Service)
cws.addMessage(LogLevel.ERROR, 'BrowserJsException', new Date(), [error])

// call super.handleError to print error the angular way to the console
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { inject, Injectable } from '@angular/core'
import { LogLevel, LogTransport } from '@shiftcode/logger'

import { CloudWatchLogV2Service } from './cloud-watch-log.service'
import { CLOUD_WATCH_LOG_V2_CONFIG } from './cloud-watch-log-config.injection-token'

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

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

log(level: LogLevel, clazzName: string, _color: string, timestamp: Date, args: unknown[]): void {
/**
* checking the log level is done in the {@link CloudWatchLogV2Service} in order to do the buffering of below-level logs.
*/
this.cloudWatchLogger.addMessage(level, clazzName, timestamp, args)
}
}
Loading