diff --git a/api/src/main/java/org/apache/flink/agents/api/configuration/AgentConfigOptions.java b/api/src/main/java/org/apache/flink/agents/api/configuration/AgentConfigOptions.java index e4871c664..016bec4fb 100644 --- a/api/src/main/java/org/apache/flink/agents/api/configuration/AgentConfigOptions.java +++ b/api/src/main/java/org/apache/flink/agents/api/configuration/AgentConfigOptions.java @@ -47,4 +47,34 @@ public class AgentConfigOptions { /** The config parameter specifies the unique identifier of job. */ public static final ConfigOption JOB_IDENTIFIER = new ConfigOption<>("job-identifier", String.class, null); + + /** + * The default event log level applied to all event types unless overridden. + * + *

Valid values: "OFF", "STANDARD", "VERBOSE". Defaults to "STANDARD". + */ + public static final ConfigOption EVENT_LOG_LEVEL = + new ConfigOption<>("eventLogLevel", String.class, "STANDARD"); + + /** + * Per-event-type log level overrides as a comma-separated string. + * + *

Format: {@code "EventTypeName=LEVEL,EventTypeName=LEVEL"}. + * + *

Example: {@code "ChatRequestEvent=VERBOSE,ContextRetrievalRequestEvent=OFF"} + */ + public static final ConfigOption EVENT_LOG_LEVELS = + new ConfigOption<>("eventLogLevels", String.class, null); + + /** + * Maximum character length for string fields in STANDARD log level output. + * + *

String fields exceeding this limit are truncated with a {@code "... [truncated, N chars + * total]"} marker. VERBOSE level ignores this setting and logs full content. A value of 0 or + * negative disables truncation. + * + *

Defaults to 1024. + */ + public static final ConfigOption EVENT_LOG_MAX_FIELD_LENGTH = + new ConfigOption<>("eventLogMaxFieldLength", Integer.class, 1024); } diff --git a/api/src/main/java/org/apache/flink/agents/api/logger/EventLogLevel.java b/api/src/main/java/org/apache/flink/agents/api/logger/EventLogLevel.java new file mode 100644 index 000000000..fee50a42d --- /dev/null +++ b/api/src/main/java/org/apache/flink/agents/api/logger/EventLogLevel.java @@ -0,0 +1,97 @@ +/* + * 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. + */ + +package org.apache.flink.agents.api.logger; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Log levels for the event logging system. + * + *

Event log levels control which events are recorded and at what detail level. Levels can be + * configured globally (as a default) and overridden per event type. + * + *

+ */ +public enum EventLogLevel { + + /** Do not log the event. */ + OFF, + + /** Log the event with standard detail. */ + STANDARD, + + /** Log the event with full detail. */ + VERBOSE; + + /** + * Returns whether logging is enabled at this level. + * + * @return {@code true} if this level is not {@link #OFF} + */ + public boolean isEnabled() { + return this != OFF; + } + + /** + * Parses a comma-separated string of per-event-type log level overrides. + * + *

The expected format is: + * + *

+     * SimpleClassName=LEVEL,SimpleClassName=LEVEL,...
+     * 
+ * + *

For example: {@code "ChatRequestEvent=VERBOSE,InputEvent=OFF"} + * + * @param configValue the comma-separated configuration string, may be {@code null} or empty + * @return an unmodifiable map of simple event type names to log levels; empty if input is null + * or empty + * @throws IllegalArgumentException if a level value is not a valid {@link EventLogLevel} + */ + public static Map parseLogLevels(String configValue) { + if (configValue == null || configValue.trim().isEmpty()) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(); + String[] entries = configValue.split(","); + for (String entry : entries) { + String trimmed = entry.trim(); + if (trimmed.isEmpty()) { + continue; + } + int eqIdx = trimmed.indexOf('='); + if (eqIdx <= 0 || eqIdx >= trimmed.length() - 1) { + throw new IllegalArgumentException( + "Invalid event log level entry: '" + + trimmed + + "'. Expected format: EventTypeName=LEVEL"); + } + String eventTypeName = trimmed.substring(0, eqIdx).trim(); + String levelStr = trimmed.substring(eqIdx + 1).trim(); + result.put(eventTypeName, EventLogLevel.valueOf(levelStr.toUpperCase())); + } + return Collections.unmodifiableMap(result); + } +} diff --git a/api/src/main/java/org/apache/flink/agents/api/logger/EventLoggerConfig.java b/api/src/main/java/org/apache/flink/agents/api/logger/EventLoggerConfig.java index f57adef6d..88e893c06 100644 --- a/api/src/main/java/org/apache/flink/agents/api/logger/EventLoggerConfig.java +++ b/api/src/main/java/org/apache/flink/agents/api/logger/EventLoggerConfig.java @@ -18,6 +18,8 @@ package org.apache.flink.agents.api.logger; +import org.apache.flink.agents.api.Event; +import org.apache.flink.agents.api.EventContext; import org.apache.flink.agents.api.EventFilter; import java.util.Collections; @@ -40,6 +42,14 @@ * .loggerType("file") * .property("baseLogDir", "/tmp/logs") * .build(); + * + * // Configure per-event-type log levels + * EventLoggerConfig levelConfig = EventLoggerConfig.builder() + * .loggerType("file") + * .defaultLogLevel(EventLogLevel.STANDARD) + * .eventLogLevel(ChatRequestEvent.class, EventLogLevel.VERBOSE) + * .eventLogLevel(ContextRetrievalRequestEvent.class, EventLogLevel.OFF) + * .build(); * } */ public final class EventLoggerConfig { @@ -47,13 +57,27 @@ public final class EventLoggerConfig { private final String loggerType; private final EventFilter eventFilter; private final Map properties; + private final EventLogLevel defaultLogLevel; + private final Map eventLogLevels; + private final int maxFieldLength; + + /** Default maximum character length for string fields at STANDARD level. */ + public static final int DEFAULT_MAX_FIELD_LENGTH = 1024; /** Private constructor - use {@link #builder()} to create instances. */ private EventLoggerConfig( - String loggerType, EventFilter eventFilter, Map properties) { + String loggerType, + EventFilter eventFilter, + Map properties, + EventLogLevel defaultLogLevel, + Map eventLogLevels, + int maxFieldLength) { this.loggerType = Objects.requireNonNull(loggerType, "Logger type cannot be null"); this.eventFilter = eventFilter == null ? EventFilter.ACCEPT_ALL : eventFilter; this.properties = Collections.unmodifiableMap(new HashMap<>(properties)); + this.defaultLogLevel = defaultLogLevel; + this.eventLogLevels = Collections.unmodifiableMap(new HashMap<>(eventLogLevels)); + this.maxFieldLength = maxFieldLength; } /** @@ -107,6 +131,64 @@ public Map getProperties() { return properties; } + /** + * Gets the default log level for events that do not have a per-type override. + * + * @return the default log level + */ + public EventLogLevel getDefaultLogLevel() { + return defaultLogLevel; + } + + /** + * Gets the per-event-type log level overrides. + * + * @return an unmodifiable map of simple event type names to log levels + */ + public Map getEventLogLevels() { + return eventLogLevels; + } + + /** + * Gets the maximum character length for string fields at STANDARD level. + * + *

A value of 0 or negative means no truncation. + * + * @return the max field length + */ + public int getMaxFieldLength() { + return maxFieldLength; + } + + /** + * Determines the effective log level for the given event. + * + *

Looks up the event's simple class name in the per-type overrides map. If no override is + * found, falls back to the default log level. + * + * @param event the event to determine the log level for + * @return the effective log level for this event + */ + public EventLogLevel getEffectiveLogLevel(Event event) { + String simpleName = event.getClass().getSimpleName(); + return eventLogLevels.getOrDefault(simpleName, defaultLogLevel); + } + + /** + * Determines whether an event should be logged based on both its log level and the event + * filter. + * + *

An event is logged only if its effective log level is enabled (not {@link + * EventLogLevel#OFF}) and the event filter accepts it. + * + * @param event the event to check + * @param context the event context + * @return {@code true} if the event should be logged + */ + public boolean shouldLog(Event event, EventContext context) { + return getEffectiveLogLevel(event).isEnabled() && eventFilter.accept(event, context); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -114,12 +196,21 @@ public boolean equals(Object o) { EventLoggerConfig that = (EventLoggerConfig) o; return Objects.equals(loggerType, that.loggerType) && Objects.equals(eventFilter, that.eventFilter) - && Objects.equals(properties, that.properties); + && Objects.equals(properties, that.properties) + && defaultLogLevel == that.defaultLogLevel + && Objects.equals(eventLogLevels, that.eventLogLevels) + && maxFieldLength == that.maxFieldLength; } @Override public int hashCode() { - return Objects.hash(loggerType, eventFilter, properties); + return Objects.hash( + loggerType, + eventFilter, + properties, + defaultLogLevel, + eventLogLevels, + maxFieldLength); } @Override @@ -132,6 +223,12 @@ public String toString() { + eventFilter + ", properties=" + properties + + ", defaultLogLevel=" + + defaultLogLevel + + ", eventLogLevels=" + + eventLogLevels + + ", maxFieldLength=" + + maxFieldLength + '}'; } @@ -145,6 +242,9 @@ public static final class Builder { private String loggerType = "file"; // Default to file logger private EventFilter eventFilter = EventFilter.ACCEPT_ALL; // Default to accept all private final Map properties = new HashMap<>(); + private EventLogLevel defaultLogLevel = EventLogLevel.STANDARD; + private final Map eventLogLevels = new HashMap<>(); + private int maxFieldLength = DEFAULT_MAX_FIELD_LENGTH; private Builder() {} @@ -207,13 +307,73 @@ public Builder properties(Map properties) { return this; } + /** + * Sets the default log level for all event types. + * + * @param level the default log level + * @return this Builder instance for method chaining + */ + public Builder defaultLogLevel(EventLogLevel level) { + this.defaultLogLevel = + Objects.requireNonNull(level, "Default log level cannot be null"); + return this; + } + + /** + * Sets the log level for a specific event type. + * + * @param eventClass the event class to configure + * @param level the log level for this event type + * @return this Builder instance for method chaining + */ + public Builder eventLogLevel(Class eventClass, EventLogLevel level) { + Objects.requireNonNull(eventClass, "Event class cannot be null"); + Objects.requireNonNull(level, "Log level cannot be null"); + this.eventLogLevels.put(eventClass.getSimpleName(), level); + return this; + } + + /** + * Sets per-event-type log level overrides from a pre-parsed map. + * + *

Keys are simple event type names (e.g., "ChatRequestEvent"). + * + * @param levels the map of event type names to log levels + * @return this Builder instance for method chaining + */ + public Builder eventLogLevels(Map levels) { + Objects.requireNonNull(levels, "Event log levels map cannot be null"); + this.eventLogLevels.putAll(levels); + return this; + } + + /** + * Sets the maximum character length for string fields at STANDARD level. + * + *

String fields exceeding this limit are truncated. VERBOSE ignores this setting. A + * value of 0 or negative disables truncation. + * + * @param maxFieldLength the maximum field length in characters + * @return this Builder instance for method chaining + */ + public Builder maxFieldLength(int maxFieldLength) { + this.maxFieldLength = maxFieldLength; + return this; + } + /** * Builds and returns an immutable EventLoggerConfig instance. * * @return a new EventLoggerConfig instance */ public EventLoggerConfig build() { - return new EventLoggerConfig(loggerType, eventFilter, properties); + return new EventLoggerConfig( + loggerType, + eventFilter, + properties, + defaultLogLevel, + eventLogLevels, + maxFieldLength); } } } diff --git a/api/src/test/java/org/apache/flink/agents/api/logger/EventLogLevelTest.java b/api/src/test/java/org/apache/flink/agents/api/logger/EventLogLevelTest.java new file mode 100644 index 000000000..7fd07c231 --- /dev/null +++ b/api/src/test/java/org/apache/flink/agents/api/logger/EventLogLevelTest.java @@ -0,0 +1,115 @@ +/* + * 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. + */ + +package org.apache.flink.agents.api.logger; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** Unit tests for {@link EventLogLevel}. */ +class EventLogLevelTest { + + @Test + void testIsEnabled() { + assertFalse(EventLogLevel.OFF.isEnabled()); + assertTrue(EventLogLevel.STANDARD.isEnabled()); + assertTrue(EventLogLevel.VERBOSE.isEnabled()); + } + + @Test + void testParseLogLevelsSimple() { + Map result = + EventLogLevel.parseLogLevels("ChatRequestEvent=VERBOSE,InputEvent=OFF"); + + assertEquals(2, result.size()); + assertEquals(EventLogLevel.VERBOSE, result.get("ChatRequestEvent")); + assertEquals(EventLogLevel.OFF, result.get("InputEvent")); + } + + @Test + void testParseLogLevelsWithWhitespace() { + Map result = + EventLogLevel.parseLogLevels(" ChatRequestEvent = VERBOSE , InputEvent = OFF "); + + assertEquals(2, result.size()); + assertEquals(EventLogLevel.VERBOSE, result.get("ChatRequestEvent")); + assertEquals(EventLogLevel.OFF, result.get("InputEvent")); + } + + @Test + void testParseLogLevelsCaseInsensitive() { + Map result = + EventLogLevel.parseLogLevels("ChatRequestEvent=verbose,InputEvent=standard"); + + assertEquals(EventLogLevel.VERBOSE, result.get("ChatRequestEvent")); + assertEquals(EventLogLevel.STANDARD, result.get("InputEvent")); + } + + @Test + void testParseLogLevelsNull() { + Map result = EventLogLevel.parseLogLevels(null); + assertTrue(result.isEmpty()); + } + + @Test + void testParseLogLevelsEmpty() { + Map result = EventLogLevel.parseLogLevels(""); + assertTrue(result.isEmpty()); + } + + @Test + void testParseLogLevelsBlank() { + Map result = EventLogLevel.parseLogLevels(" "); + assertTrue(result.isEmpty()); + } + + @Test + void testParseLogLevelsInvalidLevel() { + assertThrows( + IllegalArgumentException.class, + () -> EventLogLevel.parseLogLevels("ChatRequestEvent=INVALID")); + } + + @Test + void testParseLogLevelsInvalidFormat() { + assertThrows( + IllegalArgumentException.class, + () -> EventLogLevel.parseLogLevels("ChatRequestEvent")); + } + + @Test + void testParseLogLevelsResultIsUnmodifiable() { + Map result = + EventLogLevel.parseLogLevels("ChatRequestEvent=VERBOSE"); + + assertThrows( + UnsupportedOperationException.class, () -> result.put("foo", EventLogLevel.OFF)); + } + + @Test + void testParseLogLevelsTrailingComma() { + Map result = + EventLogLevel.parseLogLevels("ChatRequestEvent=VERBOSE,"); + + assertEquals(1, result.size()); + assertEquals(EventLogLevel.VERBOSE, result.get("ChatRequestEvent")); + } +} diff --git a/docs/content/docs/operations/configuration.md b/docs/content/docs/operations/configuration.md index 556a0e6ac..728c6ac6c 100644 --- a/docs/content/docs/operations/configuration.md +++ b/docs/content/docs/operations/configuration.md @@ -126,6 +126,9 @@ Here is the list of all built-in core configuration options. | Key | Default | Type | Description | |---------------------------|----------------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `baseLogDir` | (none) | String | Base directory for file-based event logs. If not set, uses `java.io.tmpdir/flink-agents`. | +| `eventLogLevel` | "STANDARD" | String | Default event log level for all event types. Valid values: `OFF` (do not log), `STANDARD` (log with truncated fields), `VERBOSE` (log full content). | +| `eventLogLevels` | (none) | String | Per-event-type log level overrides. Format: `EventTypeName=LEVEL,EventTypeName=LEVEL`. Example: `ChatRequestEvent=VERBOSE,ContextRetrievalRequestEvent=OFF`. | +| `eventLogMaxFieldLength` | 1024 | Integer | Maximum character length for string fields at `STANDARD` level. Fields exceeding this limit are truncated. `VERBOSE` level ignores this setting. Set to 0 to disable truncation. | | `error-handling-strategy` | ErrorHandlingStrategy.FAIL | ErrorHandlingStrategy | Strategy for handling errors during model requests, include timeout and unexpected output schema.
The option value could be: