Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/operation.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,8 @@ taking a long time for garbage collection.
completed initialization and is ready to serve requests. This means the initial
connection to the database and the first round of health check on Trino clusters
are completed. Otherwise, status code 503 is returned.

## Audit logging
Trino Gateway provides the AuditLogger interface for recording admin backend update events
to different pluggable outputs/sinks. Currently, there's implementations for logs.info and to
a database table.
22 changes: 22 additions & 0 deletions gateway-ha/src/main/java/io/trino/gateway/ha/AuditAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Licensed 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 io.trino.gateway.ha;

public enum AuditAction {
CREATE,
UPDATE,
DELETE,
ACTIVATE,
DEACTIVATE
}
19 changes: 19 additions & 0 deletions gateway-ha/src/main/java/io/trino/gateway/ha/AuditContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Licensed 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 io.trino.gateway.ha;

public enum AuditContext {
TRINO_GW_UI,
TRINO_GW_API,
}
27 changes: 27 additions & 0 deletions gateway-ha/src/main/java/io/trino/gateway/ha/AuditLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Licensed 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 io.trino.gateway.ha;

import static java.util.Objects.requireNonNullElse;

public interface AuditLogger
{
void logAudit(String user, String ip, String backendName, AuditAction action, AuditContext context, boolean success, String userComment);

static String sanitizeComment(String comment)
{
String c = requireNonNullElse(comment, "");
return c.replaceAll("\\s+", " ").trim();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Licensed 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 io.trino.gateway.ha;

import io.airlift.log.Logger;
import io.trino.gateway.ha.persistence.dao.AuditLogDao;
import org.jdbi.v3.core.Jdbi;

import java.sql.Timestamp;
import java.time.Instant;

import static java.util.Objects.requireNonNull;

public class DatabaseAuditLogger
implements AuditLogger
{
private static final Logger log = Logger.get(DatabaseAuditLogger.class);
private final AuditLogDao dao;

public DatabaseAuditLogger(Jdbi jdbi)
{
dao = requireNonNull(jdbi, "jdbi is null").onDemand(AuditLogDao.class);
}

@Override
public void logAudit(String user, String ip, String backendName, AuditAction action, AuditContext context, boolean success, String userComment)
{
try {
dao.log(user, ip, backendName, action.toString(), context.toString(), success ? 1 : 0,
AuditLogger.sanitizeComment(userComment), Timestamp.from(Instant.now()));
}
catch (Exception e) {
log.error("Failed to write audit log to database: %s", e.getMessage());
}
}
}
30 changes: 30 additions & 0 deletions gateway-ha/src/main/java/io/trino/gateway/ha/LogAuditLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Licensed 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 io.trino.gateway.ha;

import io.airlift.log.Logger;

public class LogAuditLogger
implements AuditLogger
{
private static final Logger log = Logger.get(LogAuditLogger.class);

@Override
public void logAudit(String user, String ip, String backendName, AuditAction action, AuditContext context, boolean success, String userComment)
{
String comment = AuditLogger.sanitizeComment(userComment);
log.info("GW_AUDIT_LOG: user=%s, ipAddress=%s, backend=%s, action=%s, context=%s, success=%s, userComment=%s",
user, ip, backendName, action, context, success, comment);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import io.airlift.http.client.HttpClient;
import io.trino.gateway.ha.DatabaseAuditLogger;
import io.trino.gateway.ha.clustermonitor.ClusterStatsHttpMonitor;
import io.trino.gateway.ha.clustermonitor.ClusterStatsInfoApiMonitor;
import io.trino.gateway.ha.clustermonitor.ClusterStatsJdbcMonitor;
Expand Down Expand Up @@ -90,6 +91,7 @@ public class HaGatewayProviderModule
private final GatewayBackendManager gatewayBackendManager;
private final QueryHistoryManager queryHistoryManager;
private final PathFilter pathFilter;
private final DatabaseAuditLogger dbAuditLogger;

@Override
protected void configure()
Expand All @@ -100,6 +102,7 @@ protected void configure()
binder().bind(QueryHistoryManager.class).toInstance(queryHistoryManager);
binder().bind(BackendStateManager.class).in(Scopes.SINGLETON);
binder().bind(PathFilter.class).toInstance(pathFilter);
binder().bind(DatabaseAuditLogger.class).toInstance(dbAuditLogger);
}

public HaGatewayProviderModule(HaGatewayConfiguration configuration)
Expand All @@ -125,6 +128,7 @@ public HaGatewayProviderModule(HaGatewayConfiguration configuration)
resourceGroupsManager = new HaResourceGroupsManager(connectionManager);
gatewayBackendManager = new HaGatewayManager(jdbi, configuration.getRouting());
queryHistoryManager = new HaQueryHistoryManager(jdbi, configuration.getDataStore().getJdbcUrl().startsWith("jdbc:oracle"));
dbAuditLogger = new DatabaseAuditLogger(jdbi);
}

private LbOAuthManager getOAuthManager(HaGatewayConfiguration configuration)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Licensed 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 io.trino.gateway.ha.persistence.dao;

import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;

import java.sql.Timestamp;

public interface AuditLogDao
{
@SqlUpdate("""
INSERT INTO gateway_audit_logs (user_name, ip_address, backend_name, operation, context, success, user_comment, change_time)
VALUES (:user_name, :ip_address, :backend_name, :operation, :context, :success, :user_comment, :change_time)
""")
void log(@Bind("user_name") String user_name,
@Bind("ip_address") String ip_address,
@Bind("backend_name") String backend_name,
@Bind("operation") String operation,
@Bind("context") String context,
@Bind("success") int success,
@Bind("user_comment") String user_comment,
@Bind("change_time") Timestamp change_time);
}
16 changes: 15 additions & 1 deletion gateway-ha/src/main/resources/gateway-ha-persistence-mysql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,18 @@ CREATE TABLE IF NOT EXISTS exact_match_source_selectors (

PRIMARY KEY (environment, source, query_type),
UNIQUE (source, environment, query_type, resource_group_id)
);
);

CREATE TABLE IF NOT EXISTS gateway_audit_logs (
audit_id BIGINT NOT NULL AUTO_INCREMENT,
user_name VARCHAR(256) NOT NULL,
ip_address VARCHAR(45),
backend_name VARCHAR(256) NOT NULL,
operation VARCHAR(256) NOT NULL,
context VARCHAR(256) NOT NULL,
success BOOLEAN NOT NULL,
user_comment VARCHAR(1024),
change_time TIMESTAMP NOT NULL,

PRIMARY KEY(audit_id)
);
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,18 @@ CREATE TABLE IF NOT EXISTS exact_match_source_selectors (

PRIMARY KEY (environment, source, query_type),
UNIQUE (source, environment, query_type, resource_group_id)
);
);

CREATE TABLE IF NOT EXISTS gateway_audit_logs (
audit_id BIGSERIAL,
user_name VARCHAR(256) NOT NULL,
ip_address VARCHAR(45),
backend_name VARCHAR(256) NOT NULL,
operation VARCHAR(256) NOT NULL,
context VARCHAR(256) NOT NULL,
success SMALLINT NOT NULL CHECK (success IN (0, 1)),
user_comment VARCHAR(1024),
change_time TIMESTAMP NOT NULL,

PRIMARY KEY(audit_id)
);
14 changes: 14 additions & 0 deletions gateway-ha/src/main/resources/mysql/V1__create_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,17 @@ CREATE TABLE IF NOT EXISTS exact_match_source_selectors (
PRIMARY KEY (environment, source(128), query_type),
UNIQUE (source(128), environment, query_type(128), resource_group_id)
);

CREATE TABLE IF NOT EXISTS gateway_audit_logs (
audit_id BIGINT NOT NULL AUTO_INCREMENT,
user_name VARCHAR(256) NOT NULL,
ip_address VARCHAR(45),
backend_name VARCHAR(256) NOT NULL,
operation VARCHAR(256) NOT NULL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

256 seems to be too long. isn't this one of AuditActions?
64 seems to be sufficient

context VARCHAR(256) NOT NULL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you intent by context, but it seems to be quite small. Why not text?

Copy link
Author

@kjsbot kjsbot Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context refers to where the admin made the change - ex: curl-ing, webapp, etc

success BOOLEAN NOT NULL,
user_comment VARCHAR(1024),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about text?

change_time TIMESTAMP NOT NULL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audit log should have default timestamp, no?


PRIMARY KEY(audit_id)
);
13 changes: 13 additions & 0 deletions gateway-ha/src/main/resources/oracle/V1__create_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,16 @@ CREATE TABLE exact_match_source_selectors(
PRIMARY KEY (environment, source, resource_group_id),
UNIQUE (source, environment, query_type, resource_group_id)
);

CREATE TABLE gateway_audit_logs (
audit_id NUMBER GENERATED ALWAYS as IDENTITY(START with 1 INCREMENT by 1),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
audit_id NUMBER GENERATED ALWAYS as IDENTITY(START with 1 INCREMENT by 1),
audit_id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
Image

user_name VARCHAR(256) NOT NULL,
ip_address VARCHAR2(45),
backend_name VARCHAR(256) NOT NULL,
operation VARCHAR(256) NOT NULL,
context VARCHAR(256) NOT NULL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe CLOB?

success NUMBER(1) NOT NULL CHECK (success IN (0,1)),
user_comment VARCHAR(1024),
change_time TIMESTAMP NOT NULL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about

TIMESTAMP WITH TIME ZONE CURRENT_TIMESTAMP NOT NULL

PRIMARY KEY(audit_id)
);
14 changes: 14 additions & 0 deletions gateway-ha/src/main/resources/postgresql/V1__create_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,17 @@ CREATE TABLE IF NOT EXISTS exact_match_source_selectors (
PRIMARY KEY (environment, source, query_type),
UNIQUE (source, environment, query_type, resource_group_id)
);

CREATE TABLE IF NOT EXISTS gateway_audit_logs (
audit_id BIGSERIAL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
audit_id BIGSERIAL,
audit_id BIGSERIAL PRIMARY KEY,

not so sure, but can you do this instead of line 91?

user_name VARCHAR(256) NOT NULL,
ip_address VARCHAR(45),
backend_name VARCHAR(256) NOT NULL,
operation VARCHAR(256) NOT NULL,
context VARCHAR(256) NOT NULL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSONB?

Copy link
Author

@kjsbot kjsbot Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, I think having different columns can make management simpler as well as being better for querying

success SMALLINT NOT NULL CHECK (success IN (0, 1)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not BOOLEAN?

user_comment VARCHAR(1024),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about text?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally comments will be kept sort and brief which is why the 1024 limit is kept, anything going over should be truncated

change_time TIMESTAMP NOT NULL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about TIMESTAMPTZ NOT NULL DEFAULT now()?


PRIMARY KEY(audit_id)
);
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ protected void verifyGatewaySchema(int expectedPropertiesCount)
verifyResultSetCount("SELECT name FROM resource_groups", 0);
verifyResultSetCount("SELECT user_regex FROM selectors", 0);
verifyResultSetCount("SELECT environment FROM exact_match_source_selectors", 0);
verifyResultSetCount("SELECT audit_id FROM gateway_audit_logs", 0);
}

protected void verifyResultSetCount(String sql, int expectedCount)
Expand All @@ -124,16 +125,18 @@ protected void dropAllTables()
String resourceGroupsTable = "DROP TABLE IF EXISTS resource_groups";
String selectorsTable = "DROP TABLE IF EXISTS selectors";
String exactMatchTable = "DROP TABLE IF EXISTS exact_match_source_selectors";
String gatewayAuditLogsTable = "DROP TABLE IF EXISTS gateway_audit_logs";
String flywayHistoryTable = "DROP TABLE IF EXISTS flyway_schema_history";
Handle jdbiHandle = jdbi.open();
String sql = format("SELECT 1 FROM information_schema.tables WHERE table_schema = '%s'", schema);
verifyResultSetCount(sql, 7);
verifyResultSetCount(sql, 8);
jdbiHandle.execute(gatewayBackendTable);
jdbiHandle.execute(queryHistoryTable);
jdbiHandle.execute(propertiesTable);
jdbiHandle.execute(selectorsTable);
jdbiHandle.execute(resourceGroupsTable);
jdbiHandle.execute(exactMatchTable);
jdbiHandle.execute(gatewayAuditLogsTable);
jdbiHandle.execute(flywayHistoryTable);
verifyResultSetCount(sql, 0);
jdbiHandle.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ protected void dropAllTables()
* For this reason, if you remove the double quotes on flyway_schema_history,
* you will get a table not found error.
*/
List<String> tables = ImmutableList.of("gateway_backend", "query_history", "resource_groups_global_properties", "selectors", "resource_groups", "exact_match_source_selectors", "\"flyway_schema_history\"");
List<String> tables = ImmutableList.of("gateway_backend", "query_history", "resource_groups_global_properties", "selectors", "resource_groups", "exact_match_source_selectors", "gateway_audit_logs", "\"flyway_schema_history\"");
Handle jdbiHandle = jdbi.open();
String sql = format("SELECT 1 FROM all_tables WHERE owner = '%s'", schema);
verifyResultSetCount(sql, 7);
verifyResultSetCount(sql, 8);
tables.forEach(table -> jdbiHandle.execute("DROP TABLE " + table));
verifyResultSetCount(sql, 0);
jdbiHandle.close();
Expand Down