File size: 4,998 Bytes
76289e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Logging config helpers read and normalize logger configuration.
import fs from "node:fs";
import { isRecord as isObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { resolveConfigEnvVars } from "../config/env-substitution.js";
import { resolveConfigIncludes, resolveConfigIncludesForTopLevelKey } from "../config/includes.js";
import { resolveConfigPath, resolveIncludeRoots } from "../config/paths.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { tryProcessCwd } from "../infra/safe-cwd.js";
import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js";
import { APPLIED_LOGGING_CONFIG_UNOWNED, loggingState } from "./state.js";

// Lightweight logging-config reader used before the full config runtime is safe to load.
type LoggingConfig = NonNullable<OpenClawConfig["logging"]>;

let cachedLoggingConfig:
  | {
      selector: string;
      logging: LoggingConfig | undefined;
    }
  | undefined;

export function invalidateLoggingConfigCache(): void {
  cachedLoggingConfig = undefined;
}

function resolveLoggingConfigSelector(): string {
  const env = process.env;
  return [
    env.OPENCLAW_CONFIG_PATH,
    env.OPENCLAW_STATE_DIR,
    env.OPENCLAW_HOME,
    env.OPENCLAW_PROFILE,
    env.HOME,
    env.USERPROFILE,
    env.HOMEDRIVE,
    env.HOMEPATH,
    env.PREFIX,
    env.ANDROID_DATA,
    env.OPENCLAW_TEST_FAST,
    tryProcessCwd() ?? "",
  ]
    .map((value) => value ?? "")
    .join("\0");
}

function resolvePartialDiagnosticLoggingConfig(logging: unknown): LoggingConfig | undefined {
  if (!isObjectRecord(logging)) {
    return undefined;
  }
  const partial: Record<string, unknown> = {};
  if (typeof logging.consoleStyle === "string") {
    try {
      const resolved = resolveConfigEnvVars({ consoleStyle: logging.consoleStyle });
      if (
        isObjectRecord(resolved) &&
        (resolved.consoleStyle === "pretty" ||
          resolved.consoleStyle === "compact" ||
          resolved.consoleStyle === "json")
      ) {
        partial.consoleStyle = resolved.consoleStyle;
      }
    } catch {
      // Keep resolving independent diagnostic fields.
    }
  }
  if (Array.isArray(logging.redactPatterns)) {
    try {
      const resolved = resolveConfigEnvVars({ redactPatterns: logging.redactPatterns });
      if (
        isObjectRecord(resolved) &&
        Array.isArray(resolved.redactPatterns) &&
        resolved.redactPatterns.every((entry) => typeof entry === "string")
      ) {
        partial.redactPatterns = resolved.redactPatterns;
      }
    } catch {
      // A missing variable in one pattern must not hide a separately resolvable style.
    }
  }
  return Object.keys(partial).length > 0 ? (partial as LoggingConfig) : undefined;
}

/** Reads the logging block from config, caching by resolved config path. */
export function readLoggingConfig(): LoggingConfig | undefined {
  try {
    if (loggingState.appliedConfig !== APPLIED_LOGGING_CONFIG_UNOWNED) {
      return loggingState.appliedConfig;
    }
    const selector = resolveLoggingConfigSelector();
    if (cachedLoggingConfig?.selector === selector) {
      return cachedLoggingConfig.logging;
    }
    const configPath = resolveConfigPath();
    if (!fs.existsSync(configPath)) {
      cachedLoggingConfig = { selector, logging: undefined };
      return undefined;
    }
    const parsed = parseJsonWithJson5Fallback(fs.readFileSync(configPath, "utf8"));
    const allowedRoots = resolveIncludeRoots();
    let includedConfig: unknown;
    try {
      includedConfig = resolveConfigIncludesForTopLevelKey(
        parsed,
        configPath,
        "logging",
        undefined,
        { allowedRoots },
      );
    } catch {
      // Validation commands still need a directly-authored logging style when an unrelated
      // root include is malformed. Resolve only that subtree through the same canonical rules.
      const directLogging = isObjectRecord(parsed) ? parsed.logging : undefined;
      if (directLogging === undefined) {
        return undefined;
      }
      try {
        includedConfig = resolveConfigIncludes({ logging: directLogging }, configPath, undefined, {
          allowedRoots,
        });
      } catch {
        const logging = resolvePartialDiagnosticLoggingConfig(directLogging);
        return logging;
      }
    }
    let resolvedConfig: unknown;
    try {
      resolvedConfig = resolveConfigEnvVars(includedConfig);
    } catch {
      const includedLogging = isObjectRecord(includedConfig) ? includedConfig.logging : undefined;
      const logging = resolvePartialDiagnosticLoggingConfig(includedLogging);
      return logging;
    }
    const logging = isObjectRecord(resolvedConfig) ? resolvedConfig.logging : undefined;
    const resolvedLogging = isObjectRecord(logging) ? (logging as LoggingConfig) : undefined;
    cachedLoggingConfig = {
      selector,
      logging: resolvedLogging,
    };
    return resolvedLogging;
  } catch {
    return undefined;
  }
}