INNORIX
Transfer BuilderTransfer FinderDevelopersResourcesCustomers
Start Free
INNORIX

LET FILES
MOVE THEMSELVES

INNORIX provides enterprise file infrastructure for moving and automating files across every system and environment.
Trusted by more than 5,000 enterprise and government agencies.

START HERE

  • Build the Transfer You Need
  • Find the Transfer You Need

POPULAR TRANSFERS

  • Sync Team Folders
  • Send Large Files to Clients
  • Explore Files Across Systems
  • Migrate FTP, SFTP, SCP & rsync
  • Add Transfer to Any App
  • Add Web Upload & Download
  • Build AI & Data Workflows
  • Browse All Transfers→

DEVELOPERS

  • Developer Center
  • Examples
  • API Quickstart
  • Developer Guide
  • API Reference
  • GitHub

RESOURCES

  • Resource Center
  • Product Guide
  • Integrations
  • Deploy & Manage
  • Help Center

CUSTOMERS

  • Government
  • Public Sector
  • Manufacturing
  • Engineering
  • Finance
  • Distribution
  • IT/Telecom
  • Media
  • Healthcare
  • Education

PLANS

  • Pricing

COMPANY

About Us

OTHER INNORIX PRODUCT

Al.bert — Smart Traffic AI

GLOBAL OFFICES

  • New York, USA
  • Seoul, South Korea
  • Ho Chi Minh City, Vietnam
  • View Office Locations→

(C)2026 INNORIX. All rights reserved.

  • Security
  • Status
  • Terms
  • Privacy
  • Cookies
  1. Guide
  2. Collect Logs and Diagnostic Files from Distributed Servers into a Central Location

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
  • Automating the End-to-End Workflow for AI Training Data and Model Files
  • Deploy AI Model Files to Multiple Edge Devices
  • Securely Importing and Exporting Files in Air-Gapped and Network-Segmented Environments
  • Simultaneously Distributing Approved Files to Multiple Locations
  • Transfer Files from Azure Blob to an On-Premises Server
  • Automatically Transferring DB Dumps, Backup, and Archive Files to Remote Storage
  • Centrally Collecting Files from Branches, Factories, and Edge Devices
  • Deploy CI/CD Build Artifacts to Multiple Servers
  • Move Files Between Different Cloud Storage Services
  • Transfer Files Between S3 Buckets in Different AWS Accounts
  • Provide Customer-Specific File Workspaces
  • Provide Customers with Large-File Download Links That Expire
  • Let Customers Upload Large Files Through a Browser
  • Send Large Files Directly to a Customer Workspace
  • Store Database Backup Files in Object Storage
  • Receive File Transfer Failure and Recovery Alerts in Datadog
  • Add File Transfer Capabilities to Web, App, and Business Systems
  • Run Validation, Conversion, and Follow-Up Tasks After File Arrival
  • Migrate Large Volumes of Files from NAS and File Servers to the Cloud
  • Convert FTP Batch Jobs to Managed File Flows
  • Transfer Directly from Google Cloud Storage to Amazon S3
  • Create a File Transfer Status Dashboard in Grafana
  • Automatically Verify File Integrity Using Hash Values
  • Automatically Transfer New Files to a Designated Destination
  • Send Result Files from Kubernetes to Object Storage
  • Automatically Transfer Large Files and Build Artifacts Outside Git
  • Automatically Deliver Files That Are Difficult to Manage with Git
  • Collect Logs and Diagnostic Files from Distributed Servers into a Central Location
  • Automatically Transfer Media Sources and Processing Results Step by Step
  • Scan Received Files with Microsoft Defender and Perform Follow-up Processing
  • Automate Multi-Step File Transfers in a Single Workflow
  • Automatically Transfer Media Sources and Processing Results Step by Step
  • Automatically Route Partner Files to Internal Business Systems
  • Exchange Files Regularly with Partners and Supply Chain Systems
  • Automating Recurring File Transfers Daily and Weekly
  • Automatically Resume File Transfers After a Network Interruption
  • Convert rsync Jobs to Managed File Flows
  • Schedule Recurring File Transfers
  • Distribute Software and Firmware to Multiple Devices and Review Results
  • Distributing Software Packages to Multiple Servers and Branches
  • Automatically Apply Team Folder Changes Across Multiple Devices
  • Automatically Reflect a Team Work Folder Across Multiple PCs
  • Find and Transfer Files Across Multiple Devices from One Place
  • Add Large File Upload and Download to Your Website

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 ──┼──→ Central Collection ──→ Analysis System
           │
App Server ─┤
           │
Edge Device ─┘

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.

① Generate log or diagnostic files

↓

② Identify collection targets

↓

③ Apply collection criteria

↓

④ Transfer to the central analysis environment

↓

⑤ Connect analysis and monitoring tasks

↓

⑥ 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 ──────┼──→ Central Storage
               │           │
Server ────────┤           ├──→ Analysis
               │           │
Edge ──────────┘           └──→ 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 ─────→ Result Check
      │
      └── 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)
// InnorixClient.java
public static final String BASE_URL =
        env("INNORIX_BASE_URL", "https://app.innorix.com").replaceAll("/+
quot;
, ""); public static final String WORKSPACE_ID = env("INNORIX_WORKSPACE_ID", null); public static final int STATUS_COMPLETE = 2; // States the transfer no longer moves out of public static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99); // Terminal states that are not a full success public static final Set<Integer> NOT_SUCCEEDED = Set.of(4, 5, 9, 99); private HttpRequest.Builder headers(HttpRequest.Builder builder) { builder.header("Content-Type", "application/json") .header("Authorization", "Bearer " + session.accessToken()); // When omitted the account's current workspace is used. if (workspaceId != null) builder.header("x-workspace-id", workspaceId); return builder; } /** Unwraps and returns data from the response. Throws ApiError on failure. */ public Object api(String method, String path, Object body, Map<String, Object> params) { Resp response = request(method, path, body, params); Object payload = null; try { payload = Json.parse(response.text()); } catch (RuntimeException ignored) { payload = null; } if (!response.ok()) { Map<String, Object> map = Json.asObj(payload); String message = Json.str(map, "message", Json.str(map, "error", "unknown error")); throw new ApiError(response.status, message, map); } return Json.get(payload, "data"); } /** Use the server flag when present, otherwise fall back to the status code. */ public static boolean isTerminal(Map<String, Object> record) { Boolean flag = Json.boolOrNull(record, "isTerminal"); if (flag != null) return flag; Integer status = Json.intOrNull(record, "status"); return status != null && TERMINAL.contains(status); }
// innorix-client.js
const BASE_URL = (process.env.INNORIX_BASE_URL
  || "https://app.innorix.com").replace(/\/+$/, "");
const TOKEN = process.env.INNORIX_ACCESS_TOKEN;
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID || null;

export const STATUS_COMPLETE = 2;
export const TERMINAL = new Set([2, 4, 5, 9, 99]);       // complete / error / cancelled / partial / failed
export const NOT_SUCCEEDED = new Set([4, 5, 9, 99]);

export async function api(method, path, body = null, params = null) {
  const url = new URL(BASE_URL + path);

  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (value === undefined || value === null) continue;
      url.searchParams.set(key, String(value));
    }
  }

  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${TOKEN}`,
  };

  // When omitted the account's current workspace is used.
  if (WORKSPACE_ID) headers["x-workspace-id"] = WORKSPACE_ID;

  const response = await fetch(url, {
    method,
    headers,
    body: body === null ? undefined : JSON.stringify(body),
    signal: AbortSignal.timeout(30_000),
  });

  const payload = await response.json().catch(() => ({}));

  if (!response.ok) {
    throw new Error(payload.message || `HTTP ${response.status}`);
  }

  return payload.data;
}

export function isTerminal(detail) {
  return detail.isTerminal !== undefined
    ? detail.isTerminal
    : TERMINAL.has(detail.status);
}
// InnorixClient.cs
public static readonly string BaseUrl =
    Env("INNORIX_BASE_URL", "https://app.innorix.com").TrimEnd('/');
public static readonly string WorkspaceIdFromEnv = Env("INNORIX_WORKSPACE_ID", null);

public const int StatusComplete = 2;
/// <summary>States the transfer no longer moves out of</summary>
public static readonly HashSet<int> Terminal = new HashSet<int> { 2, 4, 5, 9, 99 };
/// <summary>Terminal states that are not a full success</summary>
public static readonly HashSet<int> NotSucceeded = new HashSet<int> { 4, 5, 9, 99 };

// Applied on every request
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + Session.AccessToken);
// When omitted the account's current workspace is used.
if (WorkspaceId != null) request.Headers.TryAddWithoutValidation("x-workspace-id", WorkspaceId);

public async Task<JsonNode> ApiAsync(string method, string path, JsonNode body = null,
    IDictionary<string, object> parameters = null)
{
    Resp response = await RequestAsync(method, path, body, parameters).ConfigureAwait(false);
    JsonNode payload = null;

    try
    {
        payload = J.Parse(response.Text());
    }
    catch (Exception)
    {
        payload = null;
    }

    if (!response.Ok)
    {
        JsonObject map = J.AsObj(payload);
        string message = J.Str(map, "message", J.Str(map, "error", "unknown error"));
        throw new ApiError(response.Status, message, map);
    }

    return J.Get(payload, "data");
}

/// <summary>Use the server flag when present, otherwise fall back to the status code.</summary>
public static bool IsTerminal(JsonObject record)
{
    bool? flag = J.BoolOrNull(record, "isTerminal");
    if (flag != null) return flag.Value;

    int? status = J.IntOrNull(record, "status");
    return status != null && Terminal.Contains(status.Value);
}
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())
public static String encodePath(String deviceId, String rawPath) {
    String normalized = (rawPath == null ? "" : rawPath).replace("\\", "/");
    return deviceId + "_ino_"
            + Base64.getEncoder().encodeToString(normalized.getBytes(StandardCharsets.UTF_8));
}

public static String nowIso() {
    return Instant.now().truncatedTo(ChronoUnit.SECONDS).toString().replace("Z", ".000Z");
}
export function encodePath(deviceId, rawPath) {
  const normalized = String(rawPath ?? "").replace(/\\/g, "/");
  const token = Buffer.from(normalized, "utf8").toString("base64");
  return `${deviceId}_ino_${token}`;
}

export function nowIso() {
  return new Date().toISOString().replace(/\.\d{3}Z$/, ".000Z");
}
public static string EncodePath(string deviceId, string rawPath)
{
    string normalized = (rawPath ?? "").Replace("\\", "/");
    return deviceId + "_ino_" + Convert.ToBase64String(Encoding.UTF8.GetBytes(normalized));
}

public static string NowIso()
{
    return DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'.000Z'",
        System.Globalization.CultureInfo.InvariantCulture);
}

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 {}
public static Map<String, Object> buildFilter(List<String> exts, Long minSize, String exclude) {
    Map<String, Object> fileOption = new LinkedHashMap<>();

    if (exts != null && !exts.isEmpty()) {
        List<Object> cleaned = new ArrayList<>();
        // e.g. ["mp4","mov"] (no dot). allow=true -> whitelist
        for (String ext : exts) cleaned.add(ext.replaceAll("^\\.+", "").toLowerCase());
        fileOption.put("extension", Json.newObj("extension", cleaned, "allow", true));
    }

    if (minSize != null) {
        // over=true/equal=true -> only files at or above size
        fileOption.put("fileSize", Json.newObj("size", minSize, "over", true, "equal", true));
    }

    if (exclude != null) {
        // allow=false -> skip files whose name contains exclude (server match is case sensitive)
        fileOption.put("fileName", Json.newObj("name", exclude, "allow", false));
    }

    return fileOption.isEmpty()
            ? new LinkedHashMap<>()
            : Json.newObj("send-fileoption", fileOption);
}
export function buildFilter({ exts = null, minSize = null, exclude = null } = {}) {
  const fileOption = {};

  if (exts) {
    // Extension whitelist, without the leading dot.
    fileOption.extension = {
      extension: exts.map((e) => e.replace(/^\./, "").toLowerCase()),
      allow: true,
    };
  }

  if (minSize !== null && minSize !== undefined) {
    // over and equal both true means size or larger.
    fileOption.fileSize = { size: minSize, over: true, equal: true };
  }

  if (exclude) {
    // allow=false excludes files whose name contains this. Matching is case sensitive.
    fileOption.fileName = { name: exclude, allow: false };
  }

  return Object.keys(fileOption).length ? { "send-fileoption": fileOption } : {};
}
public static JsonObject BuildFilter(IReadOnlyList<string> exts = null,
    long? minSize = null, string exclude = null)
{
    var fileOption = new JsonObject();

    if (exts != null && exts.Count > 0)
    {
        // e.g. ["mp4","mov"] (no dot). allow=true -> whitelist
        var cleaned = new JsonArray();
        foreach (string ext in exts) cleaned.Add(ext.TrimStart('.').ToLowerInvariant());

        fileOption["extension"] = new JsonObject { ["extension"] = cleaned, ["allow"] = true };
    }

    if (minSize != null)
    {
        // over=true/equal=true -> only files at or above size
        fileOption["fileSize"] = new JsonObject
        {
            ["size"] = minSize.Value,
            ["over"] = true,
            ["equal"] = true,
        };
    }

    if (exclude != null)
    {
        // allow=false -> skip files whose name contains exclude (server match is case sensitive)
        fileOption["fileName"] = new JsonObject { ["name"] = exclude, ["allow"] = false };
    }

    return fileOption.Count == 0
        ? new JsonObject()
        : new JsonObject { ["send-fileoption"] = fileOption };
}

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"])
Map<String, Object> logFilter = InnorixClient.buildFilter(
        List.of("log", "gz"), null, ".lck");
Map<String, Object> dumpFilter = InnorixClient.buildFilter(
        List.of("core", "dmp", "hprof"), null, null);
const logFilter = buildFilter({ exts: ["log", "gz"], exclude: ".lck" });
const dumpFilter = buildFilter({ exts: ["core", "dmp", "hprof"] });
JsonObject logFilter = InnorixClient.BuildFilter(
    new[] { "log", "gz" }, exclude: ".lck");
JsonObject dumpFilter = InnorixClient.BuildFilter(
    new[] { "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")
Map<String, Object> page = client.apiObj("POST",
        "/api/devices/" + deviceId + "/files/search",
        Json.newObj("path", "/var/log/application", "pageSize", 500));

long matched = 0;
for (Object node : Json.arrOf(page, "items")) {
    Map<String, Object> item = Json.asObj(node);
    String name = Json.str(item, "name");
    if ("file".equals(Json.str(item, "type"))
            && (name.endsWith(".log") || name.endsWith(".gz"))) matched++;
}

System.out.println(matched + " matched");
const page = await client.api("POST",
  `/api/devices/${deviceId}/files/search`,
  { path: "/var/log/application", pageSize: 500 });

const matched = (page.items || []).filter((i) =>
  i.type === "file" && /\.(log|gz)$/.test(i.name));

console.log(`${matched.length} matched`);
JsonObject page = await client.ApiObjAsync("POST",
    
quot;/api/devices/{deviceId}/files/search"
, new JsonObject { ["path"] = "/var/log/application", ["pageSize"] = 500 }); int matched = J.ArrOf(page, "items").Select(J.AsObj).Count(i => J.Str(i, "type") == "file" && Regex.IsMatch(J.Str(i, "name"), @"\.(log|gz)
quot;
)); Console.WriteLine(
quot;{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))
Map<String, Object> buildCollection(String name, String source, String sourcePath,
                                    String target, String targetPath,
                                    Map<String, Object> schedule,
                                    Map<String, Object> options) {
    Map<String, Object> transferOptions = new LinkedHashMap<>(Json.newObj(
            "noSchedule", false, "target-action", "numbering",
            "send-fileoption", Json.newObj()));
    if (options != null) transferOptions.putAll(options);

    Map<String, Object> detail = Json.newObj(
            "senderId", source, "receiverId", target,
            "sourceItem", List.of(Json.newObj(
                    "hash", InnorixClient.encodePath(source, sourcePath),
                    "filePath", sourcePath, "isDir", true)),
            "targetPath", InnorixClient.encodePath(target, targetPath),
            "step", 1, "transferOptions", transferOptions);

    return Json.newObj(
            "name", name, "flowName", name,
            "transferType", "normal", "timezone", "Asia/Seoul",
            "step", 1, "isUpcoming", false,
            "details", List.of(detail),
            "schedules", List.of(schedule));
}
function buildCollection(name, source, sourcePath, target, targetPath,
  schedule, options) {
  return {
    name,
    flowName: name,
    transferType: "normal",
    timezone: "Asia/Seoul",
    step: 1,
    isUpcoming: false,
    details: [{
      senderId: source,
      receiverId: target,
      sourceItem: [{
        hash: encodePath(source, sourcePath),
        filePath: sourcePath,
        isDir: true,
      }],
      targetPath: encodePath(target, targetPath),
      step: 1,
      transferOptions: {
        noSchedule: false,
        "target-action": "numbering",
        "send-fileoption": {},
        ...(options || {}),
      },
    }],
    schedules: [schedule],
  };
}
JsonObject BuildCollection(string name, string source, string sourcePath,
    string target, string targetPath, JsonObject schedule, JsonObject options)
{
    var transferOptions = new JsonObject
    {
        ["noSchedule"] = false,
        ["target-action"] = "numbering",
        ["send-fileoption"] = new JsonObject(),
    };
    if (options != null)
        foreach (var kv in options) transferOptions[kv.Key] = kv.Value?.DeepClone();

    var detail = new JsonObject
    {
        ["senderId"] = source,
        ["receiverId"] = target,
        ["sourceItem"] = new JsonArray
        {
            new JsonObject
            {
                ["hash"] = InnorixClient.EncodePath(source, sourcePath),
                ["filePath"] = sourcePath,
                ["isDir"] = true,
            },
        },
        ["targetPath"] = InnorixClient.EncodePath(target, targetPath),
        ["step"] = 1,
        ["transferOptions"] = transferOptions,
    };

    return new JsonObject
    {
        ["name"] = name,
        ["flowName"] = name,
        ["transferType"] = "normal",
        ["timezone"] = "Asia/Seoul",
        ["step"] = 1,
        ["isUpcoming"] = false,
        ["details"] = new JsonArray { detail },
        ["schedules"] = new JsonArray { schedule },
    };
}

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)
Map<String, Object> body = buildCollection(
        "core dump", "device-app-01", "/var/crash",
        "device-central-01", "/collect/device-app-01/dump",
        Json.newObj("type", "none", "startDateType", "now",
                    "startDate", InnorixClient.nowIso(), "timezone", "Asia/Seoul"),
        new LinkedHashMap<>(dumpFilter) {{
            put("syncType", 1);
            put("watchFolderType", 1);
        }});

body.put("transferType", "sync");
Map<String, Object> detail = Json.asObj(Json.arrOf(body, "details").get(0));
Json.asObj(detail.get("transferOptions")).put("noSchedule", true);

client.api("POST", "/api/automations", body);
const body = buildCollection(
  "core dump", "device-app-01", "/var/crash",
  "device-central-01", "/collect/device-app-01/dump",
  { type: "none", startDateType: "now", startDate: nowIso(), timezone: "Asia/Seoul" },
  { ...dumpFilter, syncType: 1, watchFolderType: 1 });

body.transferType = "sync";
body.details[0].transferOptions.noSchedule = true;

await client.api("POST", "/api/automations", body);
JsonObject body = BuildCollection(
    "core dump", "device-app-01", "/var/crash",
    "device-central-01", "/collect/device-app-01/dump",
    new JsonObject
    {
        ["type"] = "none", ["startDateType"] = "now",
        ["startDate"] = InnorixClient.NowIso(), ["timezone"] = "Asia/Seoul",
    },
    new JsonObject(dumpFilter.DeepClone().AsObject())
    {
        ["syncType"] = 1,
        ["watchFolderType"] = 1,
    });

body["transferType"] = "sync";
body["details"]![0]!["transferOptions"]!["noSchedule"] = true;

await client.ApiAsync("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))
List<String[]> sources = List.of(
        new String[]{"device-app-01", "/var/log/application"},
        new String[]{"device-app-02", "/var/log/application"},
        new String[]{"device-linux-01", "/var/log/system"},
        new String[]{"device-edge-01", "/data/diagnostics"});

for (String[] entry : sources) {
    String source = entry[0], path = entry[1];
    client.api("POST", "/api/automations", buildCollection(
            "collect " + source, source, path,
            "device-central-01", "/collect/" + source,
            DAILY_4AM, logFilter));
}
const 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 (const [source, path] of SOURCES) {
  await client.api("POST", "/api/automations", buildCollection(
    `collect ${source}`, source, path,
    "device-central-01", `/collect/${source}`,
    DAILY_4AM, logFilter));
}
var sources = new[]
{
    ("device-app-01", "/var/log/application"),
    ("device-app-02", "/var/log/application"),
    ("device-linux-01", "/var/log/system"),
    ("device-edge-01", "/data/diagnostics"),
};

foreach (var (source, path) in sources)
{
    await client.ApiAsync("POST", "/api/automations", BuildCollection(
        
quot;collect {source}"
, source, path, "device-central-01",
quot;/collect/{source}"
, Daily4Am, logFilter)); }

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.log
    device-app-02/
        application.log

Analysis Integration#

Call an analysis job after collection completes

body["processors"] = [{
    "category": "run",
    "type": "http",
    "config": {
        "url": "https://internal.example.com/analyze",
        "method": "POST",
    },
}]
body.put("processors", List.of(Json.newObj(
        "category", "run",
        "type", "http",
        "config", Json.newObj(
                "url", "https://internal.example.com/analyze",
                "method", "POST"))));
body.processors = [{
  category: "run",
  type: "http",
  config: {
    url: "https://internal.example.com/analyze",
    method: "POST",
  },
}];
body["processors"] = new JsonArray
{
    new JsonObject
    {
        ["category"] = "run",
        ["type"] = "http",
        ["config"] = new JsonObject
        {
            ["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)
void onCollectHook(Map<String, Object> payload) {
    String monitorId = Json.str(payload, "monitorId");

    // If you subscribed to the completed event only, this check can be skipped
    if (monitorId != null) {
        Map<String, Object> detail = client.apiObj("GET", "/api/transfers/" + monitorId);
        if (Json.intOr(detail, "status", -1) != InnorixClient.STATUS_COMPLETE) {
            skipFailedCollection(payload);
            return;
        }
    }

    startAnalysis(payload);
}
async function onCollectHook(payload) {
  const monitorId = payload.monitorId;

  // If you subscribed to the completed event only, this check can be skipped
  if (monitorId) {
    const detail = await client.api("GET", `/api/transfers/${monitorId}`);
    if (detail.status !== STATUS_COMPLETE) return skipFailedCollection(payload);
  }

  return startAnalysis(payload);
}
async Task OnCollectHookAsync(JsonObject payload)
{
    string monitorId = J.Str(payload, "monitorId");

    // If you subscribed to the completed event only, this check can be skipped
    if (monitorId != null)
    {
        JsonObject detail = await client.ApiObjAsync("GET", "/api/transfers/" + monitorId);
        if (J.Int(detail, "status", -1) != InnorixClient.StatusComplete)
        {
            await SkipFailedCollectionAsync(payload);
            return;
        }
    }

    await StartAnalysisAsync(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)
public Map<String, Object> wait(String monitorId, int timeoutSeconds, int intervalSeconds) {
    long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;

    while (System.currentTimeMillis() < deadline) {
        Map<String, Object> detail = apiObj("GET", "/api/transfers/" + monitorId);

        if (isTerminal(detail)) return detail;

        sleep(intervalSeconds * 1000L);
    }

    throw new ApiError(0, "timeout waiting for " + monitorId);
}

public List<Map<String, Object>> failedFiles(String monitorId) {
    List<Map<String, Object>> failed = new ArrayList<>();

    for (Map<String, Object> row : transferFiles(monitorId)) {
        Integer status = Json.intOrNull(row, "status");
        if (status != null && NOT_SUCCEEDED.contains(status)) failed.add(row);
    }

    return failed;
}

public int retryFailed(String monitorId) {
    List<Map<String, Object>> rows = failedFiles(monitorId);
    if (rows.isEmpty()) return 0;

    List<Object> filesRetry = new ArrayList<>();

    for (Map<String, Object> row : rows) {
        String path = Json.str(row, "sourceFilePath");
        if (path == null) continue;
        filesRetry.add(Json.newObj("filePath", path, "isDir", Json.bool(row, "isFolder", false)));
    }

    api("POST", "/api/transfers/" + monitorId + "/retry", Json.newObj("filesRetry", filesRetry));

    return rows.size();
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function wait(monitorId, { timeout = 3600, interval = 3 } = {}) {
  const deadline = Date.now() + timeout * 1000;

  while (Date.now() < deadline) {
    const detail = await api("GET", `/api/transfers/${monitorId}`);

    if (isTerminal(detail)) return detail;

    await sleep(interval * 1000);
  }

  throw new Error(`timeout waiting for ${monitorId}`);
}

export async function failedFiles(monitorId) {
  const result = (await api("GET", `/api/transfers/${monitorId}/files`, null, {
    state: "any", size: 500,
  })) || {};

  return (result.children || []).filter((row) => NOT_SUCCEEDED.has(row.status));
}

export async function retryFailed(monitorId) {
  const rows = await failedFiles(monitorId);

  if (rows.length === 0) return 0;

  await api("POST", `/api/transfers/${monitorId}/retry`, {
    filesRetry: rows
      .filter((row) => row.sourceFilePath)
      .map((row) => ({ filePath: row.sourceFilePath, isDir: Boolean(row.isFolder) })),
  });

  return rows.length;
}
public async Task<JsonObject> WaitAsync(string monitorId, int timeoutSeconds = 3600,
    int intervalSeconds = 3)
{
    long deadline = Environment.TickCount64 + timeoutSeconds * 1000L;

    while (Environment.TickCount64 < deadline)
    {
        JsonObject detail = await ApiObjAsync("GET", "/api/transfers/" + monitorId)
            .ConfigureAwait(false);

        if (IsTerminal(detail)) return detail;

        await Task.Delay(intervalSeconds * 1000).ConfigureAwait(false);
    }

    throw new ApiError(0, "timeout waiting for " + monitorId);
}

public async Task<List<JsonObject>> FailedFilesAsync(string monitorId)
{
    List<JsonObject> rows = await TransferFilesAsync(monitorId).ConfigureAwait(false);

    return rows.Where(row =>
    {
        int? status = J.IntOrNull(row, "status");
        return status != null && NotSucceeded.Contains(status.Value);
    }).ToList();
}

/// <summary>Only callable once the transfer has reached a terminal state.</summary>
public async Task<int> RetryFailedAsync(string monitorId)
{
    List<JsonObject> rows = await FailedFilesAsync(monitorId).ConfigureAwait(false);
    if (rows.Count == 0) return 0;

    var filesRetry = new JsonArray();

    foreach (JsonObject row in rows)
    {
        string path = J.Str(row, "sourceFilePath");
        if (path == null) continue;

        filesRetry.Add(new JsonObject
        {
            ["filePath"] = path,
            ["isDir"] = J.Bool(row, "isFolder", false),
        });
    }

    await ApiAsync("POST", "/api/transfers/" + monitorId + "/retry",
        new JsonObject { ["filesRetry"] = filesRetry }).ConfigureAwait(false);

    return rows.Count;
}
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}")
List<Map<String, Object>> history(String deviceId, int days) {
    Instant end = Instant.now();
    DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
            .withZone(ZoneOffset.UTC);

    return client.paginate("/api/devices/" + deviceId + "/transfer-history",
            Json.newObj("startDate", fmt.format(end.minus(days, ChronoUnit.DAYS)),
                        "endDate", fmt.format(end)), 200, 50);
}

for (String[] entry : sources) {
    List<Map<String, Object>> rows = history(entry[0], 1);
    long failed = rows.stream().filter(r -> {
        Integer status = Json.intOrNull(r, "status");
        return status != null && InnorixClient.NOT_SUCCEEDED.contains(status);
    }).count();

    String mark = failed == 0 ? "" : "  <- needs attention";
    System.out.printf("%-20s collected %4d failed %3d%s%n",
            entry[0], rows.size(), failed, mark);
}
async function history(deviceId, days = 1) {
  const end = new Date();
  const fmt = (d) => d.toISOString().replace(/\.\d{3}Z$/, "Z");

  const rows = [];
  for await (const row of paginate(`/api/devices/${deviceId}/transfer-history`, {
    startDate: fmt(new Date(end.getTime() - days * 86400000)),
    endDate: fmt(end),
  })) rows.push(row);
  return rows;
}

for (const [source] of SOURCES) {
  const rows = await history(source);
  const failed = rows.filter((r) => NOT_SUCCEEDED.has(r.status)).length;
  const mark = failed ? "  <- needs attention" : "";
  console.log(`${source.padEnd(20)} collected ${String(rows.length).padStart(4)}`
    + ` failed ${String(failed).padStart(3)}${mark}`);
}
async Task<List<JsonObject>> HistoryAsync(string deviceId, int days = 1)
{
    DateTime end = DateTime.UtcNow;
    const string Fmt = "yyyy-MM-dd'T'HH:mm:ss'Z'";

    return await client.PaginateAsync(
quot;/api/devices/{deviceId}/transfer-history"
, new Dictionary<string, object> { ["startDate"] = end.AddDays(-days).ToString(Fmt), ["endDate"] = end.ToString(Fmt), }); } foreach (var (source, _) in sources) { List<JsonObject> rows = await HistoryAsync(source); int failed = rows.Count(r => { int? status = J.IntOrNull(r, "status"); return status != null && InnorixClient.NotSucceeded.Contains(status.Value); }); string mark = failed == 0 ? "" : " <- needs attention"; Console.WriteLine(
quot;{source,-20} collected {rows.Count,4} failed {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
PreviousAutomatically Deliver Files That Are Difficult to Manage with GitNextAutomatically Transfer Media Sources and Processing Results Step by Step

On this page

  • Getting Started
  • Basic Concept
  • Collection Flow
  • Operational Benefits
  • IT Engineers
  • Collection Environment
  • Collection Policy
  • Central Collection
  • Analysis Integration
  • Operational Review
  • Developers
  • Integration Setup
  • Select Collection Targets
  • Register Scheduled Collection
  • Register Immediate Collection
  • Register Multiple Systems
  • Analysis Integration
  • Review Collection Status