@holz/core 0.8.0 → 0.8.2
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.
- package/LICENSE +21 -0
- package/package.json +5 -5
- package/src/__tests__/logger.test.ts +0 -121
- package/src/__tests__/operators.test.ts +0 -48
- package/src/index.ts +0 -11
- package/src/logger.ts +0 -63
- package/src/operators.ts +0 -29
- package/src/types.ts +0 -161
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Jesse Gibson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holz/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "A structured and composable logger",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"sideEffects": false,
|
|
24
24
|
"files": [
|
|
25
|
-
"dist"
|
|
26
|
-
"src"
|
|
25
|
+
"dist"
|
|
27
26
|
],
|
|
28
27
|
"keywords": [
|
|
29
28
|
"structured",
|
|
@@ -42,5 +41,6 @@
|
|
|
42
41
|
"vite-plugin-dts": "^4.5.3",
|
|
43
42
|
"vite-tsconfig-paths": "^5.1.4",
|
|
44
43
|
"vitest": "^3.0.8"
|
|
45
|
-
}
|
|
46
|
-
|
|
44
|
+
},
|
|
45
|
+
"gitHead": "f099a09cd01834de9bc1714908183cbb7b828c85"
|
|
46
|
+
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { createLogger } from '../logger';
|
|
2
|
-
import { level } from '../index';
|
|
3
|
-
|
|
4
|
-
const SYSTEM_TIME = new Date('2020-06-15T12:30:00Z').getTime();
|
|
5
|
-
|
|
6
|
-
vi.setSystemTime(SYSTEM_TIME);
|
|
7
|
-
|
|
8
|
-
describe('Logger', () => {
|
|
9
|
-
it('sends structured logs to the log processor', () => {
|
|
10
|
-
const backend = vi.fn();
|
|
11
|
-
const logger = createLogger(backend);
|
|
12
|
-
|
|
13
|
-
logger.info('Hello world', { audience: 'testers' });
|
|
14
|
-
|
|
15
|
-
expect(backend).toHaveBeenCalledOnce();
|
|
16
|
-
expect(backend).toHaveBeenCalledWith({
|
|
17
|
-
timestamp: SYSTEM_TIME,
|
|
18
|
-
message: 'Hello world',
|
|
19
|
-
level: level.info,
|
|
20
|
-
origin: [],
|
|
21
|
-
context: { audience: 'testers' },
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it.each([
|
|
26
|
-
['trace' as const, 'Look, a dead fly', { urgency: 'high?' }],
|
|
27
|
-
['debug' as const, 'Made in Britain', { condition: 'fire' }],
|
|
28
|
-
['info' as const, 'I am not a window cleaner!', { state: 'panic' }],
|
|
29
|
-
['warn' as const, 'There are irregularities in the pension fund', {}],
|
|
30
|
-
['error' as const, 'Have you tried turning it off and on again?', {}],
|
|
31
|
-
['fatal' as const, 'Leadership has taken a dive', { windows: 'open' }],
|
|
32
|
-
])('correctly processes %s log messages', (method, message, context) => {
|
|
33
|
-
const backend = vi.fn();
|
|
34
|
-
const logger = createLogger(backend);
|
|
35
|
-
|
|
36
|
-
logger[method](message, context);
|
|
37
|
-
expect(backend).toHaveBeenCalledWith({
|
|
38
|
-
timestamp: SYSTEM_TIME,
|
|
39
|
-
message,
|
|
40
|
-
level: level[method],
|
|
41
|
-
context,
|
|
42
|
-
origin: [],
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('binds the log methods to the logger', () => {
|
|
47
|
-
const backend = vi.fn();
|
|
48
|
-
const logger = createLogger(backend);
|
|
49
|
-
|
|
50
|
-
const { debug, info, warn, error } = logger;
|
|
51
|
-
debug('Debugging message');
|
|
52
|
-
info('Informative message');
|
|
53
|
-
warn('Warning message');
|
|
54
|
-
error('Error message');
|
|
55
|
-
|
|
56
|
-
expect(backend).toHaveBeenCalledTimes(4);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('namespace', () => {
|
|
60
|
-
it('extends the origin of the logger', () => {
|
|
61
|
-
const backend = vi.fn();
|
|
62
|
-
const logger = createLogger(backend)
|
|
63
|
-
.namespace('signaling')
|
|
64
|
-
.namespace('socket');
|
|
65
|
-
|
|
66
|
-
logger.info('opening socket');
|
|
67
|
-
|
|
68
|
-
expect(backend).toHaveBeenCalledOnce();
|
|
69
|
-
expect(backend).toHaveBeenCalledWith({
|
|
70
|
-
timestamp: SYSTEM_TIME,
|
|
71
|
-
message: 'opening socket',
|
|
72
|
-
level: level.info,
|
|
73
|
-
origin: ['signaling', 'socket'],
|
|
74
|
-
context: {},
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('withMiddleware', () => {
|
|
80
|
-
it('allows middleware to be added', () => {
|
|
81
|
-
const backend = vi.fn();
|
|
82
|
-
const logger = createLogger(backend).withMiddleware((next) => (log) => {
|
|
83
|
-
next({ ...log, message: 'replaced' });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
logger.info('original message');
|
|
87
|
-
expect(backend).toHaveBeenCalledOnce();
|
|
88
|
-
expect(backend).toHaveBeenCalledWith({
|
|
89
|
-
timestamp: SYSTEM_TIME,
|
|
90
|
-
message: 'replaced',
|
|
91
|
-
level: level.info,
|
|
92
|
-
origin: [],
|
|
93
|
-
context: {},
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('inherits middleware from parent loggers', () => {
|
|
98
|
-
const backend = vi.fn();
|
|
99
|
-
const logger = createLogger(backend)
|
|
100
|
-
.withMiddleware((next) => (log) => {
|
|
101
|
-
log.context.parent = true;
|
|
102
|
-
next(log);
|
|
103
|
-
})
|
|
104
|
-
.namespace('child')
|
|
105
|
-
.withMiddleware((next) => (log) => {
|
|
106
|
-
log.context.child = true;
|
|
107
|
-
next(log);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
logger.info('original message');
|
|
111
|
-
expect(backend).toHaveBeenCalledOnce();
|
|
112
|
-
expect(backend).toHaveBeenCalledWith({
|
|
113
|
-
timestamp: SYSTEM_TIME,
|
|
114
|
-
message: 'original message',
|
|
115
|
-
level: level.info,
|
|
116
|
-
origin: ['child'],
|
|
117
|
-
context: { parent: true, child: true },
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { createLogger } from '../logger';
|
|
2
|
-
import { combine, filter } from '../operators';
|
|
3
|
-
import { level } from '../types';
|
|
4
|
-
|
|
5
|
-
describe('operators', () => {
|
|
6
|
-
describe('combine', () => {
|
|
7
|
-
it('combines several log processors backends into one', () => {
|
|
8
|
-
const b1 = vi.fn();
|
|
9
|
-
const b2 = vi.fn();
|
|
10
|
-
const backend = combine([b1, b2]);
|
|
11
|
-
const logger = createLogger(backend);
|
|
12
|
-
|
|
13
|
-
logger.info('tee message');
|
|
14
|
-
|
|
15
|
-
expect(b1).toHaveBeenCalledWith(
|
|
16
|
-
expect.objectContaining({ message: 'tee message' }),
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
expect(b2).toHaveBeenCalledWith(
|
|
20
|
-
expect.objectContaining({ message: 'tee message' }),
|
|
21
|
-
);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('works even if you provide no logging backend', () => {
|
|
25
|
-
const backend = combine([]);
|
|
26
|
-
const logger = createLogger(backend);
|
|
27
|
-
|
|
28
|
-
expect(() => logger.info('no backend')).not.toThrow();
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('filter', () => {
|
|
33
|
-
it('filters out logs that do not match the predicate', () => {
|
|
34
|
-
const backend = vi.fn();
|
|
35
|
-
const logger = createLogger(
|
|
36
|
-
filter((log) => log.level !== level.debug, backend),
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
logger.info('keep me');
|
|
40
|
-
logger.debug('discard');
|
|
41
|
-
|
|
42
|
-
expect(backend).toHaveBeenCalledOnce();
|
|
43
|
-
expect(backend).toHaveBeenCalledWith(
|
|
44
|
-
expect.objectContaining({ message: 'keep me' }),
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
});
|
package/src/index.ts
DELETED
package/src/logger.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
level,
|
|
3
|
-
type LogContext,
|
|
4
|
-
type LogLevel,
|
|
5
|
-
type LogProcessor,
|
|
6
|
-
type StrictContext,
|
|
7
|
-
type Logger,
|
|
8
|
-
} from './types';
|
|
9
|
-
|
|
10
|
-
const createNamespacedLogger = (
|
|
11
|
-
processor: LogProcessor,
|
|
12
|
-
namespace: ReadonlyArray<string>,
|
|
13
|
-
): Logger => {
|
|
14
|
-
const createAndSendLog = <Context extends StrictContext<Context>>(
|
|
15
|
-
level: LogLevel,
|
|
16
|
-
message: string,
|
|
17
|
-
context: Context = {} as Context,
|
|
18
|
-
) => {
|
|
19
|
-
processor({
|
|
20
|
-
timestamp: Date.now(),
|
|
21
|
-
message,
|
|
22
|
-
level,
|
|
23
|
-
origin: namespace,
|
|
24
|
-
context: context as LogContext,
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const logger: Logger = {
|
|
29
|
-
owner: namespace,
|
|
30
|
-
trace: createAndSendLog.bind(null, level.trace),
|
|
31
|
-
debug: createAndSendLog.bind(null, level.debug),
|
|
32
|
-
info: createAndSendLog.bind(null, level.info),
|
|
33
|
-
warn: createAndSendLog.bind(null, level.warn),
|
|
34
|
-
error: createAndSendLog.bind(null, level.error),
|
|
35
|
-
fatal: createAndSendLog.bind(null, level.fatal),
|
|
36
|
-
namespace: (child: string) =>
|
|
37
|
-
createNamespacedLogger(processor, namespace.concat(child)),
|
|
38
|
-
withMiddleware: (middleware) =>
|
|
39
|
-
createNamespacedLogger(middleware(processor), namespace),
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Non-enumerable to keep the repl clean.
|
|
43
|
-
const hidden = { configurable: false, enumerable: false };
|
|
44
|
-
return Object.defineProperties(logger, {
|
|
45
|
-
withMiddleware: hidden,
|
|
46
|
-
namespace: hidden,
|
|
47
|
-
trace: hidden,
|
|
48
|
-
debug: hidden,
|
|
49
|
-
info: hidden,
|
|
50
|
-
warn: hidden,
|
|
51
|
-
error: hidden,
|
|
52
|
-
fatal: hidden,
|
|
53
|
-
});
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Create a new logger. The processor is up to you. There are plugins for
|
|
58
|
-
* filtering, formatting, combining multiple processors together, and more.
|
|
59
|
-
*
|
|
60
|
-
* If you wish to use more than one processor, `combine(...)` them first.
|
|
61
|
-
*/
|
|
62
|
-
export const createLogger = (processor: LogProcessor): Logger =>
|
|
63
|
-
createNamespacedLogger(processor, []);
|
package/src/operators.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { Log, LogProcessor } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Combine several log processors into a single log processor. Each one is
|
|
5
|
-
* called in sequence. Useful for sending a single log to multiple log
|
|
6
|
-
* destinations.
|
|
7
|
-
*/
|
|
8
|
-
export const combine =
|
|
9
|
-
(processors: Array<LogProcessor>): LogProcessor =>
|
|
10
|
-
(log: Log) =>
|
|
11
|
-
processors.forEach((processor) => processor(log));
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Filter logs based on a filter function. If the function returns true, the
|
|
15
|
-
* log is kept and forwarded onto the next processor, otherwise it is
|
|
16
|
-
* discarded.
|
|
17
|
-
*/
|
|
18
|
-
export const filter =
|
|
19
|
-
(
|
|
20
|
-
/** Returns `true` to forward the log. */
|
|
21
|
-
predicate: (log: Log) => boolean,
|
|
22
|
-
/** Where to send the log if it passes the filter. */
|
|
23
|
-
processor: LogProcessor,
|
|
24
|
-
): LogProcessor =>
|
|
25
|
-
(log: Log) => {
|
|
26
|
-
if (predicate(log)) {
|
|
27
|
-
processor(log);
|
|
28
|
-
}
|
|
29
|
-
};
|
package/src/types.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A logger is made up of log processors. These processors take structured
|
|
3
|
-
* logs and do something with them. Examples are logging backends (console,
|
|
4
|
-
* file, uploads) and operators (filtering, transforming, aggregating).
|
|
5
|
-
*/
|
|
6
|
-
export interface LogProcessor {
|
|
7
|
-
/** Do something with a log message. */
|
|
8
|
-
(log: Log): unknown;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** A log message where variables are carried as structured data. */
|
|
12
|
-
export interface Log {
|
|
13
|
-
/** Unix timestamp. Includes milliseconds. */
|
|
14
|
-
readonly timestamp: number;
|
|
15
|
-
|
|
16
|
-
/** The verbatim log message. Should not contain interpolated data. */
|
|
17
|
-
readonly message: string;
|
|
18
|
-
|
|
19
|
-
/** Log severity. One of `levels.*`. */
|
|
20
|
-
readonly level: LogLevel;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Where the log message originated. Usually starts with a library or app name,
|
|
24
|
-
* followed by something more specific.
|
|
25
|
-
*
|
|
26
|
-
* @example ['logger-library', 'ConsoleBackend']
|
|
27
|
-
*/
|
|
28
|
-
readonly origin: ReadonlyArray<string>;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Key-value pairs that provide additional context for the log message. If
|
|
32
|
-
* you're tempted to interpolate data into the message, consider using log
|
|
33
|
-
* context instead.
|
|
34
|
-
*
|
|
35
|
-
* These values must be JSON serializable.
|
|
36
|
-
*
|
|
37
|
-
* Because it's easy to accidentally include unsuitable log context (e.g.
|
|
38
|
-
* redux state, PII) nested objects are not allowed. The restriction doesn't
|
|
39
|
-
* make it impossible, but it makes it harder to miss during code review.
|
|
40
|
-
*
|
|
41
|
-
* @example { userId: 123, reason: 'disconnect' }
|
|
42
|
-
*/
|
|
43
|
-
readonly context: LogContext;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const level = {
|
|
47
|
-
/** A critical failure happened and the program must exit. */
|
|
48
|
-
fatal: 60,
|
|
49
|
-
|
|
50
|
-
/** Something failed, but we can keep going. */
|
|
51
|
-
error: 50,
|
|
52
|
-
|
|
53
|
-
/** Cause for concern, but we can keep going. */
|
|
54
|
-
warn: 40,
|
|
55
|
-
|
|
56
|
-
/** High-level progress updates. */
|
|
57
|
-
info: 30,
|
|
58
|
-
|
|
59
|
-
/** Verbose update about events or control flow (usually hidden). */
|
|
60
|
-
debug: 20,
|
|
61
|
-
|
|
62
|
-
/** Extremely detailed progress updates (usually hidden). */
|
|
63
|
-
trace: 10,
|
|
64
|
-
} as const;
|
|
65
|
-
|
|
66
|
-
export type LogLevel = (typeof level)[keyof typeof level];
|
|
67
|
-
|
|
68
|
-
type JsonPrimitive = string | number | boolean | null | undefined;
|
|
69
|
-
|
|
70
|
-
export interface JsonContext {
|
|
71
|
-
[key: string]: JsonPrimitive | ReadonlyArray<JsonPrimitive>;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Custom values allowed in log context. These values don't have to be JSON.
|
|
76
|
-
* The most common case is errors, which support richer error tracking in
|
|
77
|
-
* backends that support it.
|
|
78
|
-
*
|
|
79
|
-
* This is part of the public API. Backends may use declaration merging to
|
|
80
|
-
* extend the standard attributes. It's recommended to use symbols as keys for
|
|
81
|
-
* custom features, but not required.
|
|
82
|
-
*/
|
|
83
|
-
export interface CustomContext {
|
|
84
|
-
/**
|
|
85
|
-
* Error instance associated with the log message. This supports error
|
|
86
|
-
* tracking and shows prominently in visual backends like TTY output.
|
|
87
|
-
*/
|
|
88
|
-
error: Error;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* A type narrowing constraint designed for generic constraints. Controls
|
|
93
|
-
* what fields are allowed in `log.context`, sourcing from `CustomContext`
|
|
94
|
-
* first, then falling back to any JSON value.
|
|
95
|
-
*/
|
|
96
|
-
export type StrictContext<Input> = {
|
|
97
|
-
[Key in keyof Input]: Key extends keyof CustomContext
|
|
98
|
-
? CustomContext[Key]
|
|
99
|
-
: JsonPrimitive | ReadonlyArray<JsonPrimitive>;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* A more general type expected by logging backends. Supports type narrowing,
|
|
104
|
-
* so if a known field exists, it will be used instead of the generic JSON
|
|
105
|
-
* type.
|
|
106
|
-
*/
|
|
107
|
-
export type LogContext = Partial<CustomContext> & JsonContext;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* This is the public API which generates and sends `Log` events through the
|
|
111
|
-
* user-defined processing pipeline.
|
|
112
|
-
*/
|
|
113
|
-
export interface Logger {
|
|
114
|
-
readonly owner: ReadonlyArray<string>;
|
|
115
|
-
|
|
116
|
-
/** Extend the logger to attach a class or module name to logs. */
|
|
117
|
-
namespace: (owner: string) => Logger;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Create a new logger which runs all logs through a middleware function.
|
|
121
|
-
* Useful for adding default context, doing ad-hoc filtering, or
|
|
122
|
-
* registering plugins. Only applies to this logger and its descendants.
|
|
123
|
-
*/
|
|
124
|
-
withMiddleware: (middleware: (next: LogProcessor) => LogProcessor) => Logger;
|
|
125
|
-
|
|
126
|
-
/** Log a verbose and frequent update. */
|
|
127
|
-
trace: <Context extends StrictContext<Context>>(
|
|
128
|
-
message: string,
|
|
129
|
-
context?: Context,
|
|
130
|
-
) => void;
|
|
131
|
-
|
|
132
|
-
/** Log a frequent and verbose progress update. */
|
|
133
|
-
debug: <Context extends StrictContext<Context>>(
|
|
134
|
-
message: string,
|
|
135
|
-
context?: Context,
|
|
136
|
-
) => void;
|
|
137
|
-
|
|
138
|
-
/** Log a high-level progress update. */
|
|
139
|
-
info: <Context extends StrictContext<Context>>(
|
|
140
|
-
message: string,
|
|
141
|
-
context?: Context,
|
|
142
|
-
) => void;
|
|
143
|
-
|
|
144
|
-
/** Log something concerning. */
|
|
145
|
-
warn: <Context extends StrictContext<Context>>(
|
|
146
|
-
message: string,
|
|
147
|
-
context?: Context,
|
|
148
|
-
) => void;
|
|
149
|
-
|
|
150
|
-
/** Log a failure. */
|
|
151
|
-
error: <Context extends StrictContext<Context>>(
|
|
152
|
-
message: string,
|
|
153
|
-
context?: Context,
|
|
154
|
-
) => void;
|
|
155
|
-
|
|
156
|
-
/** Log a catastrophic failure. */
|
|
157
|
-
fatal: <Context extends StrictContext<Context>>(
|
|
158
|
-
message: string,
|
|
159
|
-
context?: Context,
|
|
160
|
-
) => void;
|
|
161
|
-
}
|