Skip to content

Commit

Permalink
Add SerializableConjureDefinedError and related classes
Browse files Browse the repository at this point in the history
  • Loading branch information
Pritham Marupaka committed Oct 22, 2024
1 parent a6d4cb7 commit ab72159
Show file tree
Hide file tree
Showing 11 changed files with 412 additions and 121 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.palantir.conjure.java.api.errors;

import com.palantir.logsafe.Arg;
import java.util.List;
import javax.annotation.Nullable;

public abstract class CheckedServiceException extends Exception implements SafeLoggableError {
private static final String EXCEPTION_NAME = "CheckedServiceException";

private final ErrorType errorType;
private final List<Arg<?>> args; // unmodifiable

private final String errorInstanceId;
private final String unsafeMessage;
private final String noArgsMessage;

/**
* Creates a new exception for the given error. All {@link com.palantir.logsafe.Arg parameters} are propagated to
* clients.
*/
public CheckedServiceException(ErrorType errorType, Arg<?>... parameters) {
this(errorType, null, parameters);
}

/** As above, but additionally records the cause of this exception. */
public CheckedServiceException(ErrorType errorType, @Nullable Throwable cause, Arg<?>... args) {
super(cause);
this.errorInstanceId = SafeLoggableErrorUtils.generateErrorInstanceId(cause);
this.errorType = errorType;
this.args = SafeLoggableErrorUtils.copyToUnmodifiableList(args);
this.unsafeMessage = SafeLoggableErrorUtils.renderUnsafeMessage(EXCEPTION_NAME, errorType, args);
this.noArgsMessage = SafeLoggableErrorUtils.renderNoArgsMessage(EXCEPTION_NAME, errorType);
}

/** The {@link ErrorType} that gave rise to this exception. */
@Override
public ErrorType getErrorType() {
return errorType;
}

/** A unique identifier for (this instance of) this error. */
@Override
public String getErrorInstanceId() {
return errorInstanceId;
}

/**
* Java doc.
*/
@Override
public String getMessage() {
// Including all args here since any logger not configured with safe-logging will log this message.
return unsafeMessage;
}

/**
* Java doc.
*/
@Override
public String getLogMessage() {
// Not returning safe args here since the safe-logging framework will log this message + args explicitly.
return noArgsMessage;
}

/**
* Java doc.
*/
@Override
public List<Arg<?>> getArgs() {
return args;
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import java.util.List;

/** An exception thrown by an RPC client to indicate remote/server-side failure. */
public class RemoteException extends RuntimeException implements SafeLoggable {
public final class RemoteException extends RuntimeException implements SafeLoggable {
private static final long serialVersionUID = 1L;
private static final String ERROR_INSTANCE_ID = "errorInstanceId";
private static final String ERROR_CODE = "errorCode";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.conjure.java.api.errors;

import com.palantir.logsafe.SafeLoggable;

public interface SafeLoggableError extends SafeLoggable {
/** The {@link ErrorType} that gave rise to this exception. */
ErrorType getErrorType();

/** A unique identifier for (this instance of) this error. */
String getErrorInstanceId();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.conjure.java.api.errors;

import com.palantir.logsafe.Arg;
import com.palantir.tritium.ids.UniqueIds;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;

final class SafeLoggableErrorUtils {
private SafeLoggableErrorUtils() {}

/**
* Finds the errorInstanceId of the most recent cause if present, otherwise generates a new random identifier. Note
* that this only searches {@link Throwable#getCause() causal exceptions}, not {@link Throwable#getSuppressed()
* suppressed causes}.
*/
static String generateErrorInstanceId(@Nullable Throwable cause) {
return generateErrorInstanceId(cause, Collections.newSetFromMap(new IdentityHashMap<>()));
}

static String generateErrorInstanceId(
@Nullable Throwable cause,
// Guard against cause cycles, see Throwable.printStackTrace(PrintStreamOrWriter)
Set<Throwable> dejaVu) {
if (cause == null || !dejaVu.add(cause)) {
// we don't need cryptographically secure random UUIDs
return UniqueIds.pseudoRandomUuidV4().toString();
}
if (cause instanceof ServiceException) {
return ((ServiceException) cause).getErrorInstanceId();
}
if (cause instanceof CheckedServiceException) {
return ((CheckedServiceException) cause).getErrorInstanceId();
}
if (cause instanceof RemoteException) {
return ((RemoteException) cause).getError().errorInstanceId();
}
return generateErrorInstanceId(cause.getCause(), dejaVu);
}

static <T> List<T> copyToUnmodifiableList(T[] elements) {
if (elements == null || elements.length == 0) {
return Collections.emptyList();
}
List<T> list = new ArrayList<>(elements.length);
for (T item : elements) {
if (item != null) {
list.add(item);
}
}
return Collections.unmodifiableList(list);
}

static String renderUnsafeMessage(String exceptionName, ErrorType errorType, Arg<?>... args) {
String message = renderNoArgsMessage(exceptionName, errorType);

if (args == null || args.length == 0) {
return message;
}

StringBuilder builder = new StringBuilder();
boolean first = true;
builder.append(message).append(": {");
for (Arg<?> arg : args) {
if (arg != null) {
if (first) {
first = false;
} else {
builder.append(", ");
}
builder.append(arg.getName()).append("=").append(arg.getValue());
}
}
builder.append("}");

return builder.toString();
}

static String renderNoArgsMessage(String exceptionName, ErrorType errorType) {
return exceptionName + ": " + errorType.code() + " (" + errorType.name() + ")";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.conjure.java.api.errors;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.Serializable;
import java.util.List;
import org.immutables.value.Value;

@SuppressWarnings("ImmutablesStyle")
@JsonDeserialize(builder = SerializableConjureDefinedError.Builder.class)
@JsonSerialize(as = ImmutableSerializableConjureDefinedError.class)
@Value.Immutable
@Value.Style(overshadowImplementation = false)
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class SerializableConjureDefinedError implements Serializable {

@JsonProperty("errorCode")
public abstract String errorCode();

@JsonProperty("errorName")
public abstract String errorName();

@JsonProperty("errorInstanceId")
@Value.Default
@SuppressWarnings("checkstyle:designforextension")
public String errorInstanceId() {
return "";
}

/** A set of parameters that further explain the error. It's up to the creator of this object to serialize the value
* in SerializableConjureErrorParameter.
**/
public abstract List<SerializableConjureErrorParameter> parameters();

// In Conjure-Java - ConjureExceptions.java we'd create this object:
// public static SerializableConjureDefinedError forException(CheckedServiceException exception) {
// return builder()
// .errorCode(exception.getErrorType().code().name())
// .errorName(exception.getErrorType().name())
// .errorInstanceId(exception.getErrorInstanceId())
// .parameters(exception.getArgs().stream()
// .map(arg -> SerializableConjureErrorParameter.builder()
// .name(arg.getName())
// .serializedValue() // Serialize the parameter
// .isSafeForLogging(arg.isSafeForLogging())
// .build())
// .toList())
// .build();
// }

public static final class Builder extends ImmutableSerializableConjureDefinedError.Builder {}

public static Builder builder() {
return new Builder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.conjure.java.api.errors;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.Serializable;
import org.immutables.value.Value;

@SuppressWarnings("ImmutablesStyle")
@JsonDeserialize(builder = SerializableConjureErrorParameter.Builder.class)
@JsonSerialize(as = ImmutableSerializableConjureErrorParameter.class)
@Value.Immutable
// This ensures that build() will return the concrete ImmutableSer... and not the abstract type.
@Value.Style(overshadowImplementation = false)
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class SerializableConjureErrorParameter implements Serializable {

@JsonProperty("name")
public abstract String name();

@JsonProperty("serializedValue")
public abstract String serializedValue();

@JsonProperty("isSafeForLogging")
public abstract boolean isSafeForLogging();

public static final class Builder extends ImmutableSerializableConjureErrorParameter.Builder {}

public static Builder builder() {
return new Builder();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.palantir.logsafe.Arg;
import com.palantir.logsafe.exceptions.SafeIllegalStateException;
import org.immutables.value.Value;

import java.io.IOException;
import java.io.Serializable;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.immutables.value.Value;

/**
* A JSON-serializable representation of an exception/error, represented by its error code, an error name identifying
Expand Down Expand Up @@ -130,19 +129,6 @@ public static SerializableError forException(ServiceException exception) {
return builder.build();
}

public static SerializableError forExceptionWithSerializedArgs(ServiceException exception) {
Builder builder = new Builder()
.errorCode(exception.getErrorType().code().name())
.errorName(exception.getErrorType().name())
.errorInstanceId(exception.getErrorInstanceId());

for (Arg<?> arg : exception.getArgs()) {
builder.putParameters(arg.getName(), Objects.toString(arg.getValue()));
}

return builder.build();
}

// TODO(rfink): Remove once all error producers have switched to errorCode/errorName.
public static final class Builder extends ImmutableSerializableError.Builder {}

Expand Down
Loading

0 comments on commit ab72159

Please sign in to comment.