Skip to content
Open
7 changes: 7 additions & 0 deletions docs/operation.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Existing cluster information can also be modified using the edit button.

![trino.gateway.io/entity](./assets/trinogateway_cluster_page.png)

**Note:** When adding or modifying a backend through the UI, a comment is required. Please provide a meaningful comment describing the reason for the change.

## Graceful shutdown

Expand Down Expand Up @@ -82,6 +83,12 @@ 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.

## Database cache configuration

Trino Gateway can cache database queries to improve performance and reduce load
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.audit;

public enum AuditAction {
CREATE,
UPDATE,
DELETE,
ACTIVATE,
DEACTIVATE,
UNKNOWN
}
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.audit;

public enum AuditContext {
TRINO_GW_UI,
TRINO_GW_API
}
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.audit;

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();
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I don’t like the design where CompositeAuditLogger implements AuditLogger.

There are a few problems with this design:

  • It mixes normal binding and multibindings on the same interface (AuditLogger), which makes the configuration hard to understand:
        binder().bind(AuditLogger.class).to(CompositeAuditLogger.class).in(Scopes.SINGLETON);

        Multibinder<AuditLogger> auditLoggers = newSetBinder(binder(), AuditLogger.class);
        auditLoggers.addBinding().to(DatabaseAuditLogger.class).in(Scopes.SINGLETON);
        auditLoggers.addBinding().to(LogAuditLogger.class).in(Scopes.SINGLETON);
  • It mixes implementation details with the interface (e.g., static String sanitizeComment).
  • The roles of CompositeAuditLogger and the other loggers are different, so they should not be combined under the same interface.

I would recommend using the following approach instead:

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.audit;

import com.google.inject.Inject;
import io.airlift.log.Logger;

import java.util.Collections;
import java.util.Set;

import static java.util.Objects.requireNonNullElse;

public class CompositeAuditLogger
implements AuditLogger
{
private static final Logger log = Logger.get(CompositeAuditLogger.class);
private final Set<AuditLogger> loggers;

@Inject
public CompositeAuditLogger(Set<AuditLogger> loggers)
{
this.loggers = requireNonNullElse(loggers, Collections.emptySet());
}

@Override
public void logAudit(String user, String ip, String backend, AuditAction action, AuditContext context, boolean success, String userComment)
{
String sanitizedComment = AuditLogger.sanitizeComment(userComment);
for (AuditLogger logger : loggers) {
try {
logger.logAudit(user, ip, backend, action, context, success, sanitizedComment);
}
catch (Exception e) {
log.error(e, "Audit sink %s failed", logger.getClass().getSimpleName());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.audit;

import com.google.inject.Inject;
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;

@Inject
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,
AuditLogger.sanitizeComment(userComment), Timestamp.from(Instant.now()));
}
catch (Exception e) {
log.error("Failed to write audit log to database: %s", e.getMessage());
}
}
}
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.audit;

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
@@ -0,0 +1,128 @@
/*
* 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.config;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import io.trino.gateway.ha.audit.AuditAction;
import io.trino.gateway.ha.audit.AuditContext;

public class AdminProxyBackendConfiguration
{
private final ProxyBackendConfiguration proxyBackendConfiguration = new ProxyBackendConfiguration();
private AuditAction action;
private String comment = "";
private AuditContext context;

@JsonProperty
public String getName()
{
return proxyBackendConfiguration.getName();
}

@JsonSetter
public void setName(String name)
{
proxyBackendConfiguration.setName(name);
}

@JsonProperty
public String getProxyTo()
{
return proxyBackendConfiguration.getProxyTo();
}

@JsonSetter
public void setProxyTo(String proxyTo)
{
proxyBackendConfiguration.setProxyTo(proxyTo);
}

@JsonProperty
public String getExternalUrl()
{
return proxyBackendConfiguration.getExternalUrl();
}

@JsonSetter
public void setExternalUrl(String externalUrl)
{
proxyBackendConfiguration.setExternalUrl(externalUrl);
}

@JsonProperty
public boolean isActive()
{
return proxyBackendConfiguration.isActive();
}

@JsonSetter
public void setActive(boolean active)
{
proxyBackendConfiguration.setActive(active);
}

@JsonProperty
public String getRoutingGroup()
{
return proxyBackendConfiguration.getRoutingGroup();
}

@JsonSetter
public void setRoutingGroup(String routingGroup)
{
proxyBackendConfiguration.setRoutingGroup(routingGroup);
}

@JsonProperty
public AuditAction getAction()
{
return this.action;
}

@JsonSetter
public void setAction(AuditAction action)
{
this.action = action;
}

@JsonProperty
public String getComment()
{
return this.comment;
}

@JsonSetter
public void setComment(String comment)
{
this.comment = comment;
}

@JsonProperty
public AuditContext getContext()
{
return this.context;
}

@JsonSetter
public void setContext(AuditContext context)
{
this.context = context;
}

public ProxyBackendConfiguration getProxyBackendConfiguration()
{
return proxyBackendConfiguration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
import com.google.inject.Singleton;
import com.google.inject.multibindings.Multibinder;
import io.airlift.http.client.HttpClient;
import io.trino.gateway.ha.audit.AuditLogger;
import io.trino.gateway.ha.audit.CompositeAuditLogger;
import io.trino.gateway.ha.audit.DatabaseAuditLogger;
import io.trino.gateway.ha.audit.LogAuditLogger;
import io.trino.gateway.ha.clustermonitor.ActiveClusterMonitor;
import io.trino.gateway.ha.clustermonitor.ClusterStatsHttpMonitor;
import io.trino.gateway.ha.clustermonitor.ClusterStatsInfoApiMonitor;
Expand Down Expand Up @@ -87,6 +91,11 @@ protected void configure()
binder().bind(JdbcConnectionManager.class).in(Scopes.SINGLETON);
binder().bind(AuthorizationManager.class).in(Scopes.SINGLETON);
binder().bind(PathFilter.class).in(Scopes.SINGLETON);
binder().bind(AuditLogger.class).to(CompositeAuditLogger.class).in(Scopes.SINGLETON);

Multibinder<AuditLogger> auditLoggers = newSetBinder(binder(), AuditLogger.class);
auditLoggers.addBinding().to(DatabaseAuditLogger.class).in(Scopes.SINGLETON);
auditLoggers.addBinding().to(LogAuditLogger.class).in(Scopes.SINGLETON);

Multibinder<TrinoClusterStatsObserver> observers = newSetBinder(binder(), TrinoClusterStatsObserver.class);
observers.addBinding().to(HealthCheckObserver.class).in(Scopes.SINGLETON);
Expand Down
Loading