This is an automated email from the ASF dual-hosted git repository.
maciej pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new ae29f6c31 feat(js): add TCP/TLS integration tests and examples (#2822)
ae29f6c31 is described below
commit ae29f6c3154a12cec17c92217bb88897c744058a
Author: Atharva Lade <[email protected]>
AuthorDate: Mon Mar 9 14:24:19 2026 -0500
feat(js): add TCP/TLS integration tests and examples (#2822)
Closes #2808
---
.github/actions/node-npm/pre-merge/action.yml | 31 +++++-
examples/node/README.md | 22 ++++
examples/node/package.json | 4 +-
examples/node/src/tcp-tls/consumer.ts | 135 ++++++++++++++++++++++++
examples/node/src/tcp-tls/producer.ts | 126 +++++++++++++++++++++++
examples/node/test-examples.sh | 9 ++
foreign/node/src/e2e/tls.system.e2e.ts | 142 ++++++++++++++++++++++++++
scripts/run-node-examples-from-readme.sh | 86 +++++++++++++++-
8 files changed, 549 insertions(+), 6 deletions(-)
diff --git a/.github/actions/node-npm/pre-merge/action.yml
b/.github/actions/node-npm/pre-merge/action.yml
index 3700e2fa4..06213dfce 100644
--- a/.github/actions/node-npm/pre-merge/action.yml
+++ b/.github/actions/node-npm/pre-merge/action.yml
@@ -106,9 +106,38 @@ runs:
IGGY_SERVER_TCP_PORT: 8090
shell: bash
- - name: Stop Iggy server
+ - name: Stop Iggy server (plain)
if: always() && inputs.task == 'e2e'
uses: ./.github/actions/utils/server-stop
with:
pid-file: ${{ steps.iggy.outputs.pid_file }}
log-file: ${{ steps.iggy.outputs.log_file }}
+
+ - name: Start Iggy server (TLS)
+ id: iggy-tls
+ if: inputs.task == 'e2e'
+ uses: ./.github/actions/utils/server-start
+ with:
+ pid-file: ${{ runner.temp }}/iggy-server-tls.pid
+ log-file: ${{ runner.temp }}/iggy-server-tls.log
+ env:
+ IGGY_TCP_TLS_ENABLED: "true"
+ IGGY_TCP_TLS_CERT_FILE: core/certs/iggy_cert.pem
+ IGGY_TCP_TLS_KEY_FILE: core/certs/iggy_key.pem
+
+ - name: TLS E2E tests
+ if: inputs.task == 'e2e'
+ run: |
+ cd foreign/node
+ node --import @swc-node/register/esm-register --test
src/e2e/tls.system.e2e.ts
+ env:
+ IGGY_TCP_TLS_ENABLED: "true"
+ IGGY_TCP_ADDRESS: 127.0.0.1:8090
+ shell: bash
+
+ - name: Stop Iggy server (TLS)
+ if: always() && inputs.task == 'e2e'
+ uses: ./.github/actions/utils/server-stop
+ with:
+ pid-file: ${{ steps.iggy-tls.outputs.pid_file }}
+ log-file: ${{ steps.iggy-tls.outputs.log_file }}
diff --git a/examples/node/README.md b/examples/node/README.md
index 413156b91..e25098bba 100644
--- a/examples/node/README.md
+++ b/examples/node/README.md
@@ -107,3 +107,25 @@ npm run test:sink-data-producer
```
Demonstrates how to produce data that can be consumed by external sinks for
integration with other systems.
+
+## TLS Examples
+
+### TCP/TLS
+
+Producer and consumer examples using TLS-encrypted TCP connections with custom
CA certificates:
+
+```bash
+npm run test:tcp-tls:producer
+npm run test:tcp-tls:consumer
+```
+
+These examples require a TLS-enabled Iggy server. Start the server with:
+
+```bash
+IGGY_TCP_TLS_ENABLED=true \
+IGGY_TCP_TLS_CERT_FILE=core/certs/iggy_cert.pem \
+IGGY_TCP_TLS_KEY_FILE=core/certs/iggy_key.pem \
+cargo r --bin iggy-server
+```
+
+Demonstrates how to configure the client with `transport: 'TLS'` and a custom
CA certificate for secure communication.
diff --git a/examples/node/package.json b/examples/node/package.json
index 3d6b2b550..a4160d50b 100644
--- a/examples/node/package.json
+++ b/examples/node/package.json
@@ -19,7 +19,9 @@
"test:multi-tenant:producer": "tsx src/multi-tenant/producer.ts",
"test:multi-tenant:consumer": "tsx src/multi-tenant/consumer.ts",
"test:stream-builder": "tsx src/stream-builder/example.ts",
- "test:sink-data-producer": "tsx src/sink-data-producer/producer.ts"
+ "test:sink-data-producer": "tsx src/sink-data-producer/producer.ts",
+ "test:tcp-tls:producer": "tsx src/tcp-tls/producer.ts",
+ "test:tcp-tls:consumer": "tsx src/tcp-tls/consumer.ts"
},
"keywords": [
"iggy",
diff --git a/examples/node/src/tcp-tls/consumer.ts
b/examples/node/src/tcp-tls/consumer.ts
new file mode 100644
index 000000000..f1772c1a7
--- /dev/null
+++ b/examples/node/src/tcp-tls/consumer.ts
@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// TCP/TLS Consumer Example
+//
+// Demonstrates consuming messages over a TLS-encrypted TCP connection
+// using custom certificates from core/certs/.
+//
+// Prerequisites:
+// Start the Iggy server with TLS enabled:
+// IGGY_TCP_TLS_ENABLED=true \
+// IGGY_TCP_TLS_CERT_FILE=core/certs/iggy_cert.pem \
+// IGGY_TCP_TLS_KEY_FILE=core/certs/iggy_key.pem \
+// cargo r --bin iggy-server
+//
+// Run this example (from examples/node/):
+// DEBUG=iggy:* npx tsx src/tcp-tls/consumer.ts
+
+import { readFileSync } from 'node:fs';
+import { Client, PollingStrategy, Consumer } from 'apache-iggy';
+import { log, PARTITION_ID, STREAM_ID, TOPIC_ID } from '../utils';
+
+const BATCHES_LIMIT = 5;
+const MESSAGES_PER_BATCH = 10;
+
+async function consumeMessages(
+ client: Client,
+ streamId: number | string,
+ topicId: number | string,
+ partitionId: number,
+) {
+ const interval = 500;
+ log(
+ 'Messages will be consumed from stream: %s, topic: %s, partition: %d with
interval %d ms.',
+ streamId,
+ topicId,
+ partitionId,
+ interval,
+ );
+
+ let offset = 0;
+ let consumedBatches = 0;
+
+ while (consumedBatches < BATCHES_LIMIT) {
+ try {
+ log('Polling for messages...');
+ const polledMessages = await client.message.poll({
+ streamId,
+ topicId,
+ consumer: Consumer.Single,
+ partitionId,
+ pollingStrategy: PollingStrategy.Offset(BigInt(offset)),
+ count: MESSAGES_PER_BATCH,
+ autocommit: false,
+ });
+
+ if (!polledMessages || polledMessages.messages.length === 0) {
+ log('No messages found.');
+ consumedBatches++;
+ await new Promise(resolve => setTimeout(resolve, interval));
+ continue;
+ }
+
+ offset += polledMessages.messages.length;
+
+ for (const message of polledMessages.messages) {
+ const payload = message.payload.toString('utf8');
+ const { offset: msgOffset, timestamp } = message.headers;
+ log('Received message: %s (offset: %d, timestamp: %d)', payload,
msgOffset, timestamp);
+ }
+
+ consumedBatches++;
+ log('Consumed %d message(s) in batch %d.',
polledMessages.messages.length, consumedBatches);
+
+ await new Promise(resolve => setTimeout(resolve, interval));
+ } catch (error) {
+ log('Error consuming messages: %o', error);
+ throw error;
+ }
+ }
+
+ log('Consumed %d batches of messages, exiting.', consumedBatches);
+}
+
+async function main() {
+ // Configure the client with TLS transport.
+ // transport: 'TLS' activates TLS on the TCP connection
+ // ca: readFileSync(...) provides the CA certificate to verify the server
cert
+ // host: 'localhost' must match the server certificate CN/SAN
+ const client = new Client({
+ transport: 'TLS',
+ options: {
+ port: 8090,
+ host: 'localhost',
+ ca: readFileSync('../../core/certs/iggy_ca_cert.pem'),
+ },
+ credentials: { username: 'iggy', password: 'iggy' },
+ });
+
+ try {
+ log('TLS consumer has started, selected transport: TLS');
+ log('Connecting to Iggy server over TLS...');
+
+ await consumeMessages(client, STREAM_ID, TOPIC_ID, PARTITION_ID);
+ } catch (error) {
+ log('Error in main: %o', error);
+ process.exitCode = 1;
+ } finally {
+ await client.destroy();
+ log('Disconnected from server.');
+ }
+}
+
+process.on('unhandledRejection', (reason, promise) => {
+ log('Unhandled Rejection at: %o, reason: %o', promise, reason);
+ process.exitCode = 1;
+});
+
+main();
diff --git a/examples/node/src/tcp-tls/producer.ts
b/examples/node/src/tcp-tls/producer.ts
new file mode 100644
index 000000000..4fd3f5c93
--- /dev/null
+++ b/examples/node/src/tcp-tls/producer.ts
@@ -0,0 +1,126 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// TCP/TLS Producer Example
+//
+// Demonstrates producing messages over a TLS-encrypted TCP connection
+// using custom certificates from core/certs/.
+//
+// Prerequisites:
+// Start the Iggy server with TLS enabled:
+// IGGY_TCP_TLS_ENABLED=true \
+// IGGY_TCP_TLS_CERT_FILE=core/certs/iggy_cert.pem \
+// IGGY_TCP_TLS_KEY_FILE=core/certs/iggy_key.pem \
+// cargo r --bin iggy-server
+//
+// Run this example (from examples/node/):
+// DEBUG=iggy:* npx tsx src/tcp-tls/producer.ts
+
+import { readFileSync } from 'node:fs';
+import { Client, Partitioning } from 'apache-iggy';
+import { BATCHES_LIMIT, cleanup, initSystem, log, MESSAGES_PER_BATCH, sleep }
from '../utils';
+
+async function produceMessages(
+ client: Client,
+ streamId: number | string,
+ topicId: number | string,
+) {
+ const interval = 500;
+ log(
+ 'Messages will be sent to stream: %s, topic: %s with interval %d ms.',
+ streamId,
+ topicId,
+ interval,
+ );
+
+ let currentId = 0;
+ let sentBatches = 0;
+
+ for (; sentBatches < BATCHES_LIMIT;) {
+ const messages = Array.from({ length: MESSAGES_PER_BATCH }).map(() => {
+ currentId++;
+ return {
+ id: currentId,
+ headers: [],
+ payload: `message-${currentId}`,
+ };
+ });
+
+ try {
+ await client.message.send({
+ streamId,
+ topicId,
+ messages,
+ partition: Partitioning.Balanced,
+ });
+ } catch (error) {
+ log('Error sending messages: %o', error);
+ } finally {
+ sentBatches++;
+ log('Sent messages: %o', messages);
+ await sleep(interval);
+ }
+ }
+
+ log('Sent %d batches of messages, exiting.', sentBatches);
+}
+
+async function main() {
+ // Configure the client with TLS transport.
+ // transport: 'TLS' activates TLS on the TCP connection
+ // ca: readFileSync(...) provides the CA certificate to verify the server
cert
+ // host: 'localhost' must match the server certificate CN/SAN
+ const client = new Client({
+ transport: 'TLS',
+ options: {
+ port: 8090,
+ host: 'localhost',
+ ca: readFileSync('../../core/certs/iggy_ca_cert.pem'),
+ },
+ credentials: { username: 'iggy', password: 'iggy' },
+ });
+
+ let streamId: number | null = null;
+ let topicId = 0;
+ try {
+ log('TLS producer has started, selected transport: TLS');
+ log('Connecting to Iggy server over TLS...');
+
+ const { stream, topic } = await initSystem(client);
+ streamId = stream.id;
+ topicId = topic.id;
+
+ const pong = await client.system.ping();
+ log('Ping successful.', pong);
+
+ log('Stream ID: %s, Topic ID: %s', streamId, topicId);
+ await produceMessages(client, streamId, topicId);
+ } catch (error) {
+ log('Error in main: %o', error);
+ process.exitCode = 1;
+ } finally {
+ if (streamId) {
+ await cleanup(client, streamId, topicId);
+ }
+ await client.destroy();
+ log('Disconnected from server.');
+ }
+}
+
+await main();
diff --git a/examples/node/test-examples.sh b/examples/node/test-examples.sh
index 6fb579d40..b2a5b5984 100755
--- a/examples/node/test-examples.sh
+++ b/examples/node/test-examples.sh
@@ -133,4 +133,13 @@ test_example "Stream Builder" "npm run
test:stream-builder" 8
echo -e "\n${YELLOW}Testing Sink Data Producer${NC}"
test_example "Sink Data Producer" "npm run test:sink-data-producer" 8
+# TCP/TLS examples (require a TLS-enabled server)
+if [ "$IGGY_TCP_TLS_ENABLED" = "true" ]; then
+ echo -e "\n${YELLOW}Testing TCP/TLS Examples${NC}"
+ test_example "TCP/TLS Producer" "npm run test:tcp-tls:producer" 10
+ test_example "TCP/TLS Consumer" "npm run test:tcp-tls:consumer" 8
+else
+ echo -e "\n${YELLOW}Skipping TCP/TLS examples (set
IGGY_TCP_TLS_ENABLED=true to run)${NC}"
+fi
+
echo -e "\n${GREEN}🎉 All tests completed!${NC}"
diff --git a/foreign/node/src/e2e/tls.system.e2e.ts
b/foreign/node/src/e2e/tls.system.e2e.ts
new file mode 100644
index 000000000..d6006ef31
--- /dev/null
+++ b/foreign/node/src/e2e/tls.system.e2e.ts
@@ -0,0 +1,142 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// TLS integration tests for the Node.js SDK.
+//
+// These tests require a TLS-enabled Iggy server and are skipped by default.
+// To run them locally:
+//
+// 1. Start the server with TLS:
+// IGGY_ROOT_USERNAME=iggy IGGY_ROOT_PASSWORD=iggy \
+// IGGY_TCP_TLS_ENABLED=true \
+// IGGY_TCP_TLS_CERT_FILE=core/certs/iggy_cert.pem \
+// IGGY_TCP_TLS_KEY_FILE=core/certs/iggy_key.pem \
+// cargo r --bin iggy-server
+//
+// 2. Run the tests:
+// cd foreign/node
+// IGGY_TCP_TLS_ENABLED=true IGGY_TCP_ADDRESS=127.0.0.1:8090 \
+// node --import @swc-node/register/esm-register --test
src/e2e/tls.system.e2e.ts
+
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { after, describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import { Client } from '../client/client.js';
+import { Partitioning, Consumer, PollingStrategy } from '../wire/index.js';
+import { getIggyAddress } from '../tcp.sm.utils.js';
+
+const tlsEnabled = process.env.IGGY_TCP_TLS_ENABLED === 'true';
+
+// Path to the CA certificate. Override with E2E_ROOT_CA_CERT env var,
+// otherwise fall back to the default relative path from the repo root.
+const caCertPath = process.env.E2E_ROOT_CA_CERT
+ || resolve(process.cwd(), '../../core/certs/iggy_ca_cert.pem');
+
+const getTlsClient = () => {
+ const [host, port] = getIggyAddress();
+ const caCert = readFileSync(caCertPath);
+
+ // The server certificate is issued for 'localhost'. When IGGY_TCP_ADDRESS
uses
+ // an IP (e.g. 127.0.0.1), the default TLS hostname check would fail because
+ // the cert CN/SAN does not match an IP literal. Providing a custom
+ // checkServerIdentity that always succeeds works around this for local
testing.
+ return new Client({
+ transport: 'TLS',
+ options: {
+ port,
+ host,
+ ca: caCert,
+ checkServerIdentity: () => undefined,
+ },
+ credentials: { username: 'iggy', password: 'iggy' },
+ });
+};
+
+describe('e2e -> tls', { skip: !tlsEnabled && 'IGGY_TCP_TLS_ENABLED is not
set' }, async () => {
+
+ const c = getTlsClient();
+ const credentials = { username: 'iggy', password: 'iggy' };
+
+ it('e2e -> tls::ping', async () => {
+ assert.ok(await c.system.ping());
+ });
+
+ it('e2e -> tls::login', async () => {
+ assert.deepEqual(
+ await c.session.login(credentials),
+ { userId: 0 }
+ );
+ });
+
+ it('e2e -> tls::getStats', async () => {
+ const stats = await c.system.getStats();
+ assert.ok(stats);
+ assert.ok('processId' in stats);
+ assert.ok('hostname' in stats);
+ });
+
+ it('e2e -> tls::send and poll messages', async () => {
+ const streamName = 'tls-e2e-stream';
+ const topicName = 'tls-e2e-topic';
+
+ await c.stream.create({ name: streamName });
+ await c.topic.create({
+ streamId: streamName,
+ name: topicName,
+ partitionCount: 1,
+ compressionAlgorithm: 1,
+ });
+
+ const messages = [
+ { id: 1, headers: [], payload: 'tls-message-1' },
+ { id: 2, headers: [], payload: 'tls-message-2' },
+ { id: 3, headers: [], payload: 'tls-message-3' },
+ ];
+
+ await c.message.send({
+ streamId: streamName,
+ topicId: topicName,
+ messages,
+ partition: Partitioning.PartitionId(0),
+ });
+
+ const polled = await c.message.poll({
+ streamId: streamName,
+ topicId: topicName,
+ consumer: Consumer.Single,
+ partitionId: 0,
+ pollingStrategy: PollingStrategy.First,
+ count: 10,
+ autocommit: false,
+ });
+
+ assert.equal(polled.messages.length, 3);
+
+ await c.stream.delete({ streamId: streamName });
+ });
+
+ it('e2e -> tls::logout', async () => {
+ assert.ok(await c.session.logout());
+ });
+
+ after(() => {
+ c.destroy();
+ });
+});
diff --git a/scripts/run-node-examples-from-readme.sh
b/scripts/run-node-examples-from-readme.sh
index 443c08636..449f44d59 100755
--- a/scripts/run-node-examples-from-readme.sh
+++ b/scripts/run-node-examples-from-readme.sh
@@ -131,7 +131,7 @@ done < <(grep -E "^\`cargo r --bin iggy -- " README.md)
cd examples/node
-# Execute all example commands from README.md and examples/node/README.md and
check if they pass or fail
+# Execute all non-TLS example commands from README.md and
examples/node/README.md
for readme_file in README.md examples/node/README.md; do
if [ ! -f "${readme_file}" ]; then
continue
@@ -163,13 +163,91 @@ for readme_file in README.md examples/node/README.md; do
# Add a small delay between examples to avoid potential race conditions
sleep 2
- done < <(grep -E "^(npm run|tsx)" "${readme_file}")
+ done < <(grep -E "^(npm run|tsx)" "${readme_file}" | grep -v "tcp-tls")
done
cd "${ROOT_WORKDIR}"
-# Terminate server
-kill -TERM "$(cat ${PID_FILE})"
+# Stop the plain server before starting TLS server
+kill -TERM "$(cat ${PID_FILE})" 2>/dev/null || true
+sleep 2
+test -e ${PID_FILE} && rm ${PID_FILE}
+test -e ${LOG_FILE} && rm ${LOG_FILE}
+
+# Check if there are any TLS examples to run
+TLS_COMMANDS=""
+if [ -f "examples/node/README.md" ]; then
+ TLS_COMMANDS=$(grep -E "^(npm run|tsx)" "examples/node/README.md" | grep
"tcp-tls" || true)
+fi
+
+if [ -n "${TLS_COMMANDS}" ] && [ "${exit_code}" -eq 0 ]; then
+ echo ""
+ echo "=== Starting TLS server for TLS examples ==="
+
+ # Remove old server data for clean TLS test
+ test -d local_data && rm -fr local_data
+
+ # Start TLS-enabled server
+ IGGY_ROOT_USERNAME=iggy IGGY_ROOT_PASSWORD=iggy \
+ IGGY_TCP_TLS_ENABLED=true \
+ IGGY_TCP_TLS_CERT_FILE=core/certs/iggy_cert.pem \
+ IGGY_TCP_TLS_KEY_FILE=core/certs/iggy_key.pem \
+ ${SERVER_BIN} &>${LOG_FILE} &
+ echo $! >${PID_FILE}
+
+ # Wait for TLS server to start
+ SERVER_START_TIME=0
+ while ! grep -q "has started" ${LOG_FILE}; do
+ if [ ${SERVER_START_TIME} -gt ${TIMEOUT} ]; then
+ echo "TLS server did not start within ${TIMEOUT} seconds."
+ cat ${LOG_FILE}
+ exit 1
+ fi
+ echo "Waiting for TLS Iggy server to start... ${SERVER_START_TIME}"
+ sleep 1
+ ((SERVER_START_TIME += 1))
+ done
+
+ cd examples/node
+
+ # Run only TLS examples
+ for readme_file in README.md examples/node/README.md; do
+ if [ ! -f "${readme_file}" ]; then
+ continue
+ fi
+
+ while IFS= read -r command; do
+ command=$(echo "${command}" | tr -d '`' | sed 's/^#.*//')
+ if [ -z "${command}" ]; then
+ continue
+ fi
+
+ echo -e "\e[33mChecking TLS example command from
${readme_file}:\e[0m ${command}"
+ echo ""
+
+ set +e
+ eval "${command}"
+ exit_code=$?
+ set -e
+
+ if [ ${exit_code} -ne 0 ]; then
+ echo ""
+ echo -e "\e[31mTLS example command failed:\e[0m ${command}"
+ echo ""
+ break 2
+ fi
+ sleep 2
+
+ done < <(grep -E "^(npm run|tsx)" "${readme_file}" | grep "tcp-tls")
+ done
+
+ cd "${ROOT_WORKDIR}"
+
+ # Terminate TLS server
+ kill -TERM "$(cat ${PID_FILE})" 2>/dev/null || true
+fi
+
+# Cleanup
test -e ${PID_FILE} && rm ${PID_FILE}
# If everything is ok remove log and pid files otherwise cat server log