| $schemaVersion | 2 | 2 | integer | Configuration schema version. Set to 2. Files without this field are migrated from v0; deprecated keys are remapped on every load. |
| supervisionUrls | | [] | string \| string[] | string or strings array containing global connection URIs to OCPP-J servers (`ws://`, `wss://`, `http://`, `https://` or `ws+unix://`; no `#` fragment or `?` query, since the charging station identity is appended to the URL; a `ws+unix://` URL separates the socket path from the request path with one `:`, e.g. `ws+unix:///path/to/socket.sock:/OCPP16`) |
| supervisionUrlDistribution | round-robin/random/charging-station-affinity | charging-station-affinity | string | supervision urls distribution policy to simulated charging stations |
-| log | | {<br />"enabled": true,<br />"file": "logs/combined.log",<br />"errorFile": "logs/error.log",<br />"statisticsInterval": 60,<br />"level": "info",<br />"console": false,<br />"format": "simple",<br />"rotate": true<br />} | {<br />enabled?: boolean;<br />file?: string;<br />errorFile?: string;<br />statisticsInterval?: number;<br />level?: string;<br />console?: boolean;<br />format?: string;<br />rotate?: boolean;<br />maxFiles?: string \| number;<br />maxSize?: string \| number;<br />} | Log configuration section:<br />- _enabled_: enable logging<br />- _file_: log file relative path<br />- _errorFile_: error log file relative path<br />- _statisticsInterval_: seconds between charging stations statistics output in the logs<br />- _level_: emerg/alert/crit/error/warning/notice/info/debug [winston](https://github.com/winstonjs/winston) logging level</br >- _console_: output logs on the console<br />- _format_: [winston](https://github.com/winstonjs/winston) log format<br />- _rotate_: enable daily log files rotation<br />- _maxFiles_: maximum number of log files: https://github.com/winstonjs/winston-daily-rotate-file#options<br />- _maxSize_: maximum size of log files in bytes, or units of kb, mb, and gb: https://github.com/winstonjs/winston-daily-rotate-file#options |
+| log | | {<br />"enabled": true,<br />"file": "logs/combined.log",<br />"errorFile": "logs/error.log",<br />"statisticsInterval": 60,<br />"level": "info",<br />"console": false,<br />"format": "simple",<br />"rotate": true<br />} | {<br />enabled?: boolean;<br />file?: string;<br />errorFile?: string;<br />statisticsInterval?: number;<br />level?: string;<br />console?: boolean;<br />format?: string;<br />rotate?: boolean;<br />maxFiles?: string \| number;<br />maxSize?: string \| number;<br />} | Log configuration section:<br />- _enabled_: enable logging<br />- _file_: log file relative path<br />- _errorFile_: error log file relative path<br />- _statisticsInterval_: seconds between charging stations statistics output in the logs<br />- _level_: error/warn/info/http/verbose/debug/silly [winston](https://github.com/winstonjs/winston) logging level (the syslog names emerg/alert/crit/warning/notice are accepted and mapped to error/error/error/warn/info)</br >- _console_: output logs on the console<br />- _format_: [winston](https://github.com/winstonjs/winston) log format<br />- _rotate_: enable daily log files rotation<br />- _maxFiles_: maximum number of log files: https://github.com/winstonjs/winston-daily-rotate-file#options<br />- _maxSize_: maximum size of log files in bytes, or units of kb, mb, and gb: https://github.com/winstonjs/winston-daily-rotate-file#options |
| worker | | {<br />"processType": "workerSet",<br />"startDelay": 500,<br />"elementAddDelay": 0,<br />"elementsPerWorker": 'auto',<br />"poolMinSize": 4,<br />"poolMaxSize": 16<br />} | {<br />processType?: WorkerProcessType;<br />startDelay?: number;<br />elementAddDelay?: number;<br />elementsPerWorker?: number \| 'auto' \| 'all';<br />poolMinSize?: number;<br />poolMaxSize?: number;<br />resourceLimits?: ResourceLimits;<br />} | Worker configuration section:<br />- _processType_: worker threads process type (`workerSet`/`fixedPool`/`dynamicPool`)<br />- _startDelay_: milliseconds to wait at worker threads startup (only for `workerSet` worker threads process type)<br />- _elementAddDelay_: milliseconds to wait between charging station add<br />- _elementsPerWorker_: number of charging stations per worker threads for the `workerSet` process type (`auto` means (number of stations) / (number of CPUs) \* 1.5 if (number of stations) > (number of CPUs), otherwise 1; `all` means a unique worker will run all charging stations)<br />- _poolMinSize_: worker threads pool minimum number of threads</br >- _poolMaxSize_: worker threads pool maximum number of threads<br />- _resourceLimits_: worker threads [resource limits](https://nodejs.org/api/worker_threads.html#new-workerfilename-options) object option |
| uiServer | | {<br />"enabled": false,<br />"type": "ws",<br />"version": "1.1",<br />"accessPolicy": {<br />"requireTlsForNonLoopback": true,<br />"trustedProxies": [],<br />"allowLoopbackProxy": false,<br />"allowedHosts": [],<br />"allowedOrigins": []<br />},<br />"options": {<br />"host": "localhost",<br />"port": 8080<br />}<br />} | {<br />enabled?: boolean;<br />type?: ApplicationProtocol;<br />version?: ApplicationProtocolVersion;<br />accessPolicy?: {<br />requireTlsForNonLoopback?: boolean;<br />trustedProxies?: string[];<br />allowLoopbackProxy?: boolean;<br />allowedHosts?: string[];<br />allowedOrigins?: string[];<br />};<br />options?: ServerOptions;<br />authentication?: {<br />enabled: boolean;<br />type: AuthenticationType;<br />username?: string;<br />password?: string;<br />};<br />metrics?: {<br />enabled?: boolean;<br />softSampleCap?: number;<br />};<br />securityHeaders?: {<br />strictTransportSecurity?: string \| false;<br />};<br />} | UI server configuration section:<br />- _enabled_: enable UI server<br />- _type_: 'ws', 'mcp' or 'http' (deprecated)<br />- _version_: HTTP version '1.1' or '2.0' (ws and mcp transports only support '1.1')<br />- _accessPolicy_: gateway access policy. Loopback request sources are allowed in plaintext; non-loopback sources require TLS termination by a reverse proxy:<br /> - _requireTlsForNonLoopback_: reject non-loopback plaintext requests; the check honors `X-Forwarded-Proto` or `Forwarded: proto=` from a trusted proxy, non-loopback requests without forwarded protocol headers are denied as `tls-required`<br /> - _trustedProxies_: IPv4 or IPv6 literals of the immediate reverse proxies whose forwarded headers are honored (hostnames and CIDR ranges are not accepted; only single-hop forwarded chains are honored); a compromised entry can bypass per-client rate limiting by varying `X-Forwarded-For`<br /> - _allowLoopbackProxy_: accept forwarded headers when the immediate peer is loopback AND listed in _trustedProxies_ (e.g. `['127.0.0.1', '::1']`)<br /> - _allowedHosts_: explicit Host header allowlist; mitigates DNS rebinding when the UI server is exposed through a browser-facing host<br /> - _allowedOrigins_: explicit Origin header allowlist; when empty, the request Origin's URL hostname falls back to matching against _allowedHosts_<br />- _options_: node.js net module [listen options](https://nodejs.org/api/net.html#serverlistenoptions-callback)<br />- _authentication_: authentication type configuration section<br />- _metrics_: opt-in Prometheus `/metrics` endpoint (served on the configured `uiServer.type` listener — `http`, `ws` or `mcp`):<br /> - _enabled_: enable the `/metrics` endpoint<br /> - _softSampleCap_: soft cardinality cap above which a single warn is logged per scrape (default 5000)<br />- _securityHeaders_: opt-in security response headers emitted on every UI-server HTTP response path served over secure transport, across the `http`, `ws` and `mcp` transports (denials, WebSocket upgrade rejections, `http` success responses and the `/metrics` endpoint), except MCP JSON-RPC success bodies (written by the MCP SDK transport); absent by default so the default response behavior is unchanged:<br /> - _strictTransportSecurity_: `Strict-Transport-Security` (HSTS) header value, emitted verbatim (and unvalidated) when set to a non-empty string; `false` (default) or an empty string omits the header. Emission is gated on secure transport — direct TLS or a trusted-proxy-forwarded `https`/`wss` protocol — per [RFC 6797](https://www.rfc-editor.org/rfc/rfc6797) §7.2 (HSTS must not be sent over non-secure transport), so it is omitted over plaintext (e.g. loopback development). Recommended production value behind a TLS-terminating reverse proxy or native TLS: `"max-age=31536000; includeSubDomains"`. Caution: `includeSubDomains` extends the policy to every subdomain of the served host and `preload` (with submission to hstspreload.org) is effectively irreversible (removal propagates through browser preload lists over ~6–12 months); only use `includeSubDomains`/`preload` on a dedicated hostname you fully control, never on a shared host (including `localhost`) |
| performanceStorage | | {<br />"enabled": true,<br />"type": "none",<br />} | {<br />enabled?: boolean;<br />type?: string;<br />uri?: string;<br />} | Performance storage configuration section:<br />- _enabled_: enable performance storage<br />- _type_: 'jsonfile', 'mongodb' or 'none'<br />- _uri_: storage URI |
// Sub-schemas
// ---------------------------------------------------------------
+/**
+ * Logging levels of the winston logger: the logger is created with winston's
+ * default `npm` levels, so only these names select a severity threshold.
+ */
+const LOG_LEVELS = ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'] as const
+type LogLevel = (typeof LOG_LEVELS)[number]
+
+/**
+ * Syslog level names accepted for backward compatibility and mapped to the
+ * `npm` level of the same severity. winston has no such levels in its `npm`
+ * set: passed as is, they silence the console and the main log file, errors
+ * included (only the error file transport, which sets its own `error` level,
+ * still logs errors).
+ */
+const SYSLOG_LOG_LEVEL_ALIASES = {
+ alert: 'error',
+ crit: 'error',
+ emerg: 'error',
+ notice: 'info',
+ warning: 'warn',
+} as const satisfies Record<string, LogLevel>
+
+type SyslogLogLevelAlias = keyof typeof SYSLOG_LOG_LEVEL_ALIASES
+
+const isSyslogLogLevelAlias = (level: string): level is SyslogLogLevelAlias =>
+ has(level, SYSLOG_LOG_LEVEL_ALIASES)
+
+const LogLevelSchema = z
+ .enum([...LOG_LEVELS, ...(Object.keys(SYSLOG_LOG_LEVEL_ALIASES) as SyslogLogLevelAlias[])])
+ .transform((level): LogLevel =>
+ isSyslogLogLevelAlias(level) ? SYSLOG_LOG_LEVEL_ALIASES[level] : level
+ )
+
/**
* LogConfiguration — winston logger configuration section.
+ * `level` is a winston `npm` logging level (`LOG_LEVELS`); the syslog
+ * aliases (`SYSLOG_LOG_LEVEL_ALIASES`) are mapped to the `npm` level of
+ * the same severity.
* `maxFiles` and `maxSize` accept `number | string` (winston-daily-rotate-file
* units like '14d', '20m').
*/
errorFile: z.string().optional(),
file: z.string().optional(),
format: z.string().optional(),
- level: z
- .enum(['emerg', 'alert', 'crit', 'error', 'warning', 'notice', 'info', 'debug'])
- .optional(),
+ level: LogLevelSchema.optional(),
maxFiles: z.union([z.number(), z.string()]).optional(),
maxSize: z.union([z.number(), z.string()]).optional(),
rotate: z.boolean().optional(),
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
+import { Writable } from 'node:stream'
import { afterEach, describe, it } from 'node:test'
+import { createLogger, transports, config as winstonConfig } from 'winston'
import { WebSocket } from 'ws'
import {
} from '../../src/utils/index.js'
import {
ConfigurationSchema,
+ has,
SupervisionUrlSchema,
UI_SERVER_ACCESS_POLICY_DEFAULTS,
+ validateConfiguration,
WorkerConfigurationSchema,
} from '../../src/utils/index.js'
import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
BAD_FIXTURES,
buildFullConfiguration,
buildMinimalConfiguration,
+ buildV1WithDeprecatedKey,
} from './helpers/ConfigurationFixtures.js'
import { TEST_SUPERVISION_URL, TEST_SUPERVISION_URL_ALT_PORT } from './TestNetworkConstants.js'
assert.ok(!result.success)
})
- await it('should accept valid log.level values', () => {
+ await it('should accept the winston npm log.level values', () => {
+ for (const level of ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly']) {
+ const result = ConfigurationSchema.safeParse(buildMinimalConfiguration({ log: { level } }))
+ assert.ok(result.success, `Expected log.level '${level}' to be valid`)
+ assert.strictEqual(result.data.log?.level, level)
+ }
+ })
+
+ await it('should map the syslog log.level names to the winston npm level of the same severity', () => {
+ for (const [level, expected] of [
+ ['emerg', 'error'],
+ ['alert', 'error'],
+ ['crit', 'error'],
+ ['warning', 'warn'],
+ ['notice', 'info'],
+ ]) {
+ const result = ConfigurationSchema.safeParse(buildMinimalConfiguration({ log: { level } }))
+ assert.ok(result.success, `Expected log.level '${level}' to be valid`)
+ assert.strictEqual(result.data.log?.level, expected)
+ }
+ })
+
+ await it('should map a deprecated logLevel through the syslog aliases', () => {
+ const result = validateConfiguration(
+ buildV1WithDeprecatedKey('logLevel', 'warning'),
+ 'loglevel.json'
+ )
+ assert.strictEqual(result.log?.level, 'warn')
+ })
+
+ await it('should only produce log.level values the winston logger knows', () => {
for (const level of [
'emerg',
'alert',
'crit',
'error',
'warning',
+ 'warn',
'notice',
'info',
+ 'http',
+ 'verbose',
'debug',
+ 'silly',
]) {
const result = ConfigurationSchema.safeParse(buildMinimalConfiguration({ log: { level } }))
assert.ok(result.success, `Expected log.level '${level}' to be valid`)
+ const parsedLevel = result.data.log?.level
+ assert.ok(parsedLevel != null && has(parsedLevel, winstonConfig.npm.levels))
+ // a logger at any accepted level must still emit errors
+ const lines: string[] = []
+ const logger = createLogger({
+ level: parsedLevel,
+ transports: [
+ new transports.Stream({
+ stream: new Writable({
+ write (chunk: Buffer, _encoding, callback) {
+ lines.push(chunk.toString())
+ callback()
+ },
+ }),
+ }),
+ ],
+ })
+ logger.error('error message')
+ assert.strictEqual(lines.length, 1, `Expected log.level '${level}' to log errors`)
+ logger.close()
}
})
await it('should reject invalid log.level', () => {
- const result = ConfigurationSchema.safeParse(
- buildMinimalConfiguration({ log: { level: 'verbose' } })
- )
- assert.ok(!result.success)
+ for (const level of ['trace', 'WARN', '']) {
+ const result = ConfigurationSchema.safeParse(buildMinimalConfiguration({ log: { level } }))
+ assert.ok(!result.success, `Expected log.level '${level}' to be invalid`)
+ }
})
})