Collect Logs and Diagnostic Files from Distributed Servers into a Central Location

Collect logs and diagnostic files from multiple systems for centralized review, analytics, filtering, scheduling, and operational checks.

IT EngineersDevelopers

Getting Started#

Basic Concept#

Collect logs and diagnostic files from multiple systems into one analysis environment

Servers and applications generate logs, core dumps, error reports, and other diagnostic files during operation.

Central log and core dump collection gathers files generated by each system according to configured criteria and automatically transfers them to a designated central analysis environment.

Connecting multiple servers and applications to a single collection flow lets you review diagnostic data centrally and use it for analysis.

Server A
Server B
App Server
Edge Device
Central Collection
Analysis System

Collection Flow#

Automate the flow from file creation to delivery into the central analysis environment

When a log or diagnostic file is created on a source system, collection starts based on the file type, path, and configured execution conditions.

Collected files are transferred to central storage or an analysis environment. After collection completes, the files can be passed to downstream analysis and monitoring tasks.

  1. Generate log or diagnostic files
  2. Identify collection targets
  3. Apply collection criteria
  4. Transfer to the central analysis environment
  5. Connect analysis and monitoring tasks
  6. Review execution results

This creates a single operational flow for collecting diagnostic files from multiple systems and using them downstream.

Operational Benefits#

Centralize distributed diagnostic data and connect it to analysis workflows

Connecting diagnostic files from multiple systems to a central collection environment lets you manage file locations, collection status, and analysis targets in one flow.

Category Per-System Management Central Collection
File Location Check paths on each system Manage centrally in the collection environment
Collection Execution Run tasks per system Collect automatically based on conditions
Analysis Preparation Transfer required files individually Pass collected files to the analysis environment
Status Review Check results per system Review overall collection status

This creates an operational flow from file creation → central collection → analysis integration → result verification.

IT Engineers#

Collection Environment#

Connect systems where logs and diagnostic files are generated

First, connect the servers and application environments where logs, core dumps, and diagnostic files are generated to the collection flow.

Specify the file locations on each system to define the source paths used by the central collection job.

Collection Environment Typical Files
Application Server Application logs and error reports
Operations Server System logs and diagnostic files
Processing Server Job logs and processing results
Failure Analysis Environment Core dumps and error data
Edge Device Field logs and diagnostic data

Devices

  • Application Server/var/log/application
  • Linux Server/var/log/system
  • Edge Device/data/diagnostics

Collection Policy#

Configure collection criteria by file type and priority

After connecting the collection environment, define which files to collect and under what conditions.

Specify collection targets by file extension, path, and create or modify events, then configure processing order and execution criteria based on file type and importance.

File Type Collection Criteria Processing Flow
Standard Log Schedule or file change Scheduled collection
Error Log Create or modify detection Connect to analysis workflow
Core Dump File creation Priority collection
Diagnostic File Configured path and conditions Connect to analysis and monitoring
File Event
Collection Policy
Log File
Standard Collection
Error Report
Analysis Flow
Core Dump
Priority Collection

Central Collection#

Transfer diagnostic files from multiple systems to a central analysis environment

Transfer files from each system to a central storage location according to the configured collection criteria.

Bring files from multiple systems into one central environment, then connect downstream tasks based on file type or analysis purpose.

Application
Database
Server
Edge
Central Storage
Analysis
Monitoring

Analysis Integration#

Connect collected files to downstream analysis and monitoring tasks

After files are collected centrally, completed files can be used immediately by analysis systems or monitoring environments.

Using collection completion as the execution condition for the next task lets you extend the workflow from file transfer into analysis.

Collection Completed
File Available
Analysis
Monitoring
Result Tracking

Operational Review#

Review collection status and execution results, then rerun required tasks

When collection runs, use Runs and detailed execution history to review per-system file processing status and results.

Review the collection environment, file paths, connection state, and processing results together. For jobs that require additional review, take the necessary action based on the details and rerun the required operation.

Collection Run
Status Check
Completed
Review Required
View Details
Source / Path / Connection Check
Run Again
Result Check
Item Details
Collection System Server or device where the file was generated
Collection Target Log, core dump, or diagnostic file
Execution Status Current job status and progress
Processing Result Number and total size of collected files
Target Environment Central storage and analysis location
Execution History Results of collection and downstream tasks

Developers#

Filter logs and core dumps by type and collect them on a central host

Integration Setup#

Prepare shared API calls and path encoding

import os
import requests

BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")   # optional; falls back to the current workspace

STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99}          # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}


def api(method, path, body=None, params=None):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {TOKEN}",
    }

    if WORKSPACE_ID:
        headers["x-workspace-id"] = WORKSPACE_ID

    response = requests.request(
        method, BASE_URL + path,
        headers=headers, json=body, params=params, timeout=30,
    )

    payload = response.json() if response.content else {}

    if not response.ok:
        raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")

    return payload.get("data")


def is_terminal(detail):
    return detail.get("isTerminal", detail.get("status") in TERMINAL)
import base64
import time


def encode_path(device_id, raw_path):
    normalized = str(raw_path or "").replace("\\", "/")
    token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
    return f"{device_id}_ino_{token}"


def now_iso():
    return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())

Use the following values to determine transfer status. There are five terminal states, and Complete (2) is the successful state.

Status Value Meaning Terminal
2 Complete Yes
4 Error Yes
5 Cancelled Yes
9 Partial Complete Yes
99 Failed Yes
1, 6, 12, 13 Starting, Transferring, Synchronizing, Receiving No

Select Collection Targets#

Select only the files to collect by extension and size

Log folders often contain files that do not need to be collected. Define filters so only required files are transferred.

def build_filter(exts=None, min_size=None, exclude=None):
    file_option = {}

    if exts:
        # extension whitelist, without the leading dot
        file_option["extension"] = {
            "extension": [e.lstrip(".").lower() for e in exts],
            "allow": True,
        }

    if min_size is not None:
        # over and equal both True means size or larger
        file_option["fileSize"] = {"size": min_size, "over": True, "equal": True}

    if exclude:
        # allow=False excludes files whose name contains this. Server matching is case sensitive.
        file_option["fileName"] = {"name": exclude, "allow": False}

    return {"send-fileoption": file_option} if file_option else {}

Use send-fileoption.extension for extension filters. The send-filetype-cus regular expression matches only the file name without its extension, so it does not work as an extension filter.

Filter Location Behavior
Extension send-fileoption.extension With allow: true, transfer only files with these extensions
Size send-fileoption.fileSize Use over and equal to define inclusive size thresholds
Name send-fileoption.fileName With allow: false, exclude files containing the specified text

When multiple filters are provided, they are combined with AND. Only files that match every condition are transferred.

LOG_FILTER = build_filter(exts=["log", "gz"], exclude=".lck")
DUMP_FILTER = build_filter(exts=["core", "dmp", "hprof"])

Logs are often rotated and compressed as .gz files, so include both the original and compressed extensions. Use a name condition to exclude lock files.

Use search to verify that the filters match the intended files before enabling collection.

page = api("POST", f"/api/devices/{device_id}/files/search",
           {"path": "/var/log/application", "pageSize": 500})

matched = [i for i in page["items"]
           if i["type"] == "file" and i["name"].endswith((".log", ".gz"))]

print(f"{len(matched)} matched")

Register Scheduled Collection#

Collect standard logs at a specified time

def build_collection(name, source, source_path, target, target_path,
                     schedule, options):
    return {
        "name": name,
        "flowName": name,
        "transferType": "normal",
        "timezone": "Asia/Seoul",
        "step": 1,
        "isUpcoming": False,
        "details": [
            {
                "senderId": source,
                "receiverId": target,
                "sourceItem": [
                    {
                        "hash": encode_path(source, source_path),
                        "filePath": source_path,
                        "isDir": True,
                    }
                ],
                "targetPath": encode_path(target, target_path),
                "step": 1,
                "transferOptions": {
                    "noSchedule": False,
                    "target-action": "numbering",
                    "send-fileoption": {},
                    **options,
                },
            }
        ],
        "schedules": [schedule],
    }


DAILY_4AM = {
    "type": "day",
    "startDateType": "now",
    "hour": "04",
    "minute": "00",
    "ampm": "am",
    "startDate": now_iso(),
    "timezone": "Asia/Seoul",
}

api("POST", "/api/automations", build_collection(
    "daily log", "device-app-01", "/var/log/application",
    "device-central-01", "/collect/device-app-01",
    DAILY_4AM, LOG_FILTER))

There are four required considerations when creating the automation request.

Item Configuration
isUpcoming Must be false. The server default of true ignores the schedule in the request and replaces it with a one-time five-minute schedule. Steps with triggerAutomation are forced to false by the server, so you only need to set it explicitly on the first step when no trigger is present.
step Include it at both the top level and in details. It identifies the hop position within the flow.
sourceItem Include both hash (path token) and filePath (plain-text path).
syncType Include it inside transferOptions. 1 is one-way and 2 is two-way.

Registration succeeds even if all four fields are omitted, but runtime behavior changes. If a recurring schedule runs only once, check isUpcoming first.

Use numbering for diagnostic files so each collection cycle is preserved. With overwrite, rotating logs with the same name would replace one another.

Register Immediate Collection#

Collect a core dump as soon as it is created

Core dumps are most useful immediately after a failure, so collect them as soon as they are created. Register a real-time monitoring automation with transferType set to sync and include syncType and watchFolderType in transferOptions.

body = build_collection(
    "core dump", "device-app-01", "/var/crash",
    "device-central-01", "/collect/device-app-01/dump",
    {"type": "none", "startDateType": "now",
     "startDate": now_iso(), "timezone": "Asia/Seoul"},
    {**DUMP_FILTER, "syncType": 1, "watchFolderType": 1})

body["transferType"] = "sync"
body["details"][0]["transferOptions"]["noSchedule"] = True

api("POST", "/api/automations", body)

Place syncType and watchFolderType inside transferOptions. The monitored path is read from sourceItem[0].filePath, so the plain-text path inserted by build_collection becomes the monitored location.

The agent treats the file as fully written once its size stops changing, then emits the event. Large files such as core dumps may take longer to trigger, which prevents partially written files from being transferred.

Register Multiple Systems#

Store source servers in a list and register them in bulk

When you have dozens of servers, creating each collection job manually is impractical. Store them in a list and iterate over it.

SOURCES = [
    ("device-app-01", "/var/log/application"),
    ("device-app-02", "/var/log/application"),
    ("device-linux-01", "/var/log/system"),
    ("device-edge-01", "/data/diagnostics"),
]

for source, path in SOURCES:
    api("POST", "/api/automations", build_collection(
        f"collect {source}", source, path,
        "device-central-01", f"/collect/{source}",
        DAILY_4AM, LOG_FILTER))

Include the device identifier in the destination path. Servers may generate files with the same name, such as application.log, so storing everything in one path would make the source impossible to distinguish.

  • /collect/
  • device-app-01/
  • application.logLOG
  • device-app-02/
  • application.logLOG

Analysis Integration#

Call an analysis job after collection completes

body["processors"] = [{
    "category": "run",
    "type": "http",
    "config": {
        "url": "https://internal.example.com/analyze",
        "method": "POST",
    },
}]

Specify category and type, and place url, method, and body inside config.

The callback is sent after the transfer completes. Process the receiving endpoint as follows.

def on_collect_hook(payload):
    monitor_id = payload.get("monitorId")

    # If you subscribed to the completed event only, this check can be skipped
    if monitor_id:
        detail = api("GET", f"/api/transfers/{monitor_id}")

        if detail["status"] != STATUS_COMPLETE:
            return skip_failed_collection(payload)

    start_analysis(payload)

Review Collection Status#

Review collection counts and failures by system

def wait(monitor_id, timeout=3600, interval=3):
    deadline = time.time() + timeout

    while time.time() < deadline:
        detail = api("GET", f"/api/transfers/{monitor_id}")

        if is_terminal(detail):
            return detail

        time.sleep(interval)

    raise TimeoutError(monitor_id)


def failed_files(monitor_id):
    result = api("GET", f"/api/transfers/{monitor_id}/files", params={
        "state": "any", "size": 500,
    }) or {}

    return [r for r in (result.get("children") or [])
            if r.get("status") in NOT_SUCCEEDED]


def retry_failed(monitor_id):
    rows = failed_files(monitor_id)

    if not rows:
        return 0

    api("POST", f"/api/transfers/{monitor_id}/retry", {
        "filesRetry": [
            {"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
            for r in rows
        ]
    })

    return len(rows)
from collections import Counter
from datetime import datetime, timedelta, timezone


def history(device_id, days=1):
    end = datetime.now(timezone.utc)
    fmt = "%Y-%m-%dT%H:%M:%SZ"

    return list(paginate(f"/api/devices/{device_id}/transfer-history", params={
        "startDate": (end - timedelta(days=days)).strftime(fmt),
        "endDate": end.strftime(fmt),
    }))


for source, _ in SOURCES:
    rows = history(source)
    failed = [r for r in rows if r.get("status") in NOT_SUCCEEDED]
    mark = "" if not failed else "  <- needs attention"
    print(f"{source:20} collected {len(rows):>4} failed {len(failed):>3}{mark}")

Diagnostic-file collection is most valuable during an incident, but collection itself may also fail at that moment. Monitor collection failures separately.

Item Details
Collection Target Extension and file-name filters
Collection Method Scheduled run or create-event detection
Destination Path Storage location separated by device
Analysis Integration Task called after collection
Collection Failure Failure count and retry by system