From 644c5d3ce0adecb2ae3a6fdda59baa4ecf4d32b3 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Thu, 5 Aug 2021 10:02:53 -0400 Subject: [PATCH] Implement the Witchcraft Logging API Idea plugin (#309) * Implement the Witchcraft Logging API Idea plugin * Add generated changelog entries * defensive check * depends on the platform Co-authored-by: svc-changelog --- build.gradle | 1 + changelog/@unreleased/pr-309.v2.yml | 5 + settings.gradle | 1 + versions.props | 4 + witchcraft-logging-idea/build.gradle | 62 +++++ .../logging/format/CombineWithLogVisitor.java | 84 +++++++ .../logging/format/EventLogFormatter.java | 39 ++++ .../witchcraft/logging/format/Formatting.java | 72 ++++++ .../logging/format/LogFormatter.java | 53 +++++ .../witchcraft/logging/format/LogParser.java | 123 ++++++++++ .../witchcraft/logging/format/LogVisitor.java | 100 ++++++++ .../logging/format/LogVisitors.java | 40 ++++ .../logging/format/MetricLogFormatter.java | 47 ++++ .../logging/format/RequestLogFormatter.java | 60 +++++ .../logging/format/ServiceLogFormatter.java | 80 +++++++ .../logging/format/SupplierLogVisitor.java | 74 ++++++ .../logging/format/TraceLogFormatter.java | 40 ++++ .../format/WrappedLogDelegatingVisitor.java | 75 ++++++ .../logging/idea/ConsoleViewCalculator.java | 56 +++++ .../WitchcraftConsoleViewContentTypes.java | 64 ++++++ .../logging/idea/WitchcraftLogFilter.java | 90 ++++++++ .../WitchcraftLogFormatFilterProvider.java | 33 +++ .../logging/idea/WitchcraftLogFormatter.java | 74 ++++++ .../logging/idea/WitchcraftLogSettings.java | 65 ++++++ .../idea/WitchcraftLogSettingsManager.java | 30 +++ .../WitchcraftLogSettingsManagerImpl.java | 49 ++++ .../idea/WitchcraftLogSettingsPanel.form | 50 ++++ .../idea/WitchcraftLogSettingsPanel.java | 81 +++++++ .../idea/WitchcraftLogSettingsStyle.java | 30 +++ .../src/main/resources/META-INF/plugin.xml | 16 ++ .../logging/format/EventLogFormatterTest.java | 38 +++ .../logging/format/LogParserTest.java | 147 ++++++++++++ .../format/MetricLogFormatterTest.java | 39 ++++ .../format/RequestLogFormatterTest.java | 43 ++++ .../format/ServiceLogFormatterTest.java | 60 +++++ .../witchcraft/logging/format/TestData.java | 26 +++ .../logging/format/TraceLogFormatterTest.java | 46 ++++ .../idea/WitchcraftLogFormatFilterTest.java | 216 ++++++++++++++++++ ...itchcraftLogSettingsSerializationTest.java | 82 +++++++ 39 files changed, 2295 insertions(+) create mode 100644 changelog/@unreleased/pr-309.v2.yml create mode 100644 witchcraft-logging-idea/build.gradle create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/CombineWithLogVisitor.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/EventLogFormatter.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/Formatting.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogFormatter.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogParser.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogVisitor.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogVisitors.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/MetricLogFormatter.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/RequestLogFormatter.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/ServiceLogFormatter.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/SupplierLogVisitor.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/TraceLogFormatter.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/WrappedLogDelegatingVisitor.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/ConsoleViewCalculator.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftConsoleViewContentTypes.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFilter.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatFilterProvider.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatter.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettings.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsManager.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsManagerImpl.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsPanel.form create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsPanel.java create mode 100644 witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsStyle.java create mode 100644 witchcraft-logging-idea/src/main/resources/META-INF/plugin.xml create mode 100644 witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/EventLogFormatterTest.java create mode 100644 witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/LogParserTest.java create mode 100644 witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/MetricLogFormatterTest.java create mode 100644 witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/RequestLogFormatterTest.java create mode 100644 witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/ServiceLogFormatterTest.java create mode 100644 witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/TestData.java create mode 100644 witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/TraceLogFormatterTest.java create mode 100644 witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatFilterTest.java create mode 100644 witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsSerializationTest.java diff --git a/build.gradle b/build.gradle index be042ca4..99d22684 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ buildscript { classpath 'com.palantir.gradle.gitversion:gradle-git-version:0.12.3' classpath 'com.palantir.javaformat:gradle-palantir-java-format:2.2.0' classpath 'gradle.plugin.org.inferred:gradle-processors:3.3.0' + classpath 'org.jetbrains.intellij.plugins:gradle-intellij-plugin:1.1.4' } } diff --git a/changelog/@unreleased/pr-309.v2.yml b/changelog/@unreleased/pr-309.v2.yml new file mode 100644 index 00000000..5ae29687 --- /dev/null +++ b/changelog/@unreleased/pr-309.v2.yml @@ -0,0 +1,5 @@ +type: feature +feature: + description: Implement the Witchcraft Logging API Idea plugin + links: + - https://github.com/palantir/witchcraft-api/pull/309 diff --git a/settings.gradle b/settings.gradle index d6187038..eab329d8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,3 +4,4 @@ include 'witchcraft-health-api' include 'witchcraft-health-api:witchcraft-health-api-objects' include 'witchcraft-logging-api' include 'witchcraft-logging-api:witchcraft-logging-api-objects' +include 'witchcraft-logging-idea' diff --git a/versions.props b/versions.props index 84ac2784..e9cba7b0 100644 --- a/versions.props +++ b/versions.props @@ -1,2 +1,6 @@ com.palantir.conjure.java:* = 6.1.0 com.palantir.conjure:conjure = 4.16.1 +org.immutables:* = 2.8.8 +org.assertj:assertj-core = 3.20.2 +org.junit.jupiter:* = 5.7.2 +org.mockito:* = 3.11.2 \ No newline at end of file diff --git a/witchcraft-logging-idea/build.gradle b/witchcraft-logging-idea/build.gradle new file mode 100644 index 00000000..b90af0ac --- /dev/null +++ b/witchcraft-logging-idea/build.gradle @@ -0,0 +1,62 @@ +apply plugin: 'java-library' +apply plugin: 'maven-publish' +apply plugin: 'org.jetbrains.intellij' +apply plugin: 'org.inferred.processors' +apply plugin: 'com.palantir.external-publish-jar' + +sourceCompatibility = 11 + +dependencies { + implementation project(':witchcraft-logging-api:witchcraft-logging-api-objects'), { + exclude group: 'org.slf4j' + } + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' + + annotationProcessor 'org.immutables:value' + compileOnly 'org.immutables:value-annotations' + + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.mockito:mockito-core' +} + +versionRecommendations { + excludeConfigurations 'idea' +} + +versionsLock { + // 'org.jetbrains.intellij' creates a dependency on *IntelliJ*, which GCV cannot resolve + disableJavaPluginDefaults() +} + +def minimumIntellijBuild = '211.7628.21' // 2021.1.3 +intellij { + pluginName = 'witchcraft-logging-idea' + version = minimumIntellijBuild + updateSinceUntilBuild = false +} + +patchPluginXml { + pluginDescription = "Renders Witchcraft logging api logs from structured data into human-readable text." + version = project.version + sinceBuild = minimumIntellijBuild +} + +publishPlugin { + token = System.env.JETBRAINS_PLUGIN_REPO_TOKEN +} + +check.dependsOn buildPlugin, verifyPlugin +tasks.publishPlugin.onlyIf { versionDetails().isCleanTag && System.env.JETBRAINS_PLUGIN_REPO_TOKEN != null } +tasks.publish.dependsOn publishPlugin +buildSearchableOptions.enabled = false +// Prevent nebula.maven-publish from trying to publish components.java - we are publishing our own different artifact +ext."nebulaPublish.maven.jar" = false +publishing { + publications { + nebula(MavenPublication) { + artifact buildPlugin + } + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/CombineWithLogVisitor.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/CombineWithLogVisitor.java new file mode 100644 index 00000000..597ef64f --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/CombineWithLogVisitor.java @@ -0,0 +1,84 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.palantir.witchcraft.api.logging.AuditLogV2; +import com.palantir.witchcraft.api.logging.DiagnosticLogV1; +import com.palantir.witchcraft.api.logging.EventLogV2; +import com.palantir.witchcraft.api.logging.MetricLogV1; +import com.palantir.witchcraft.api.logging.RequestLogV2; +import com.palantir.witchcraft.api.logging.ServiceLogV1; +import com.palantir.witchcraft.api.logging.TraceLogV1; +import java.util.Optional; +import java.util.function.BiFunction; + +final class CombineWithLogVisitor implements LogVisitor { + private final LogVisitor first; + private final LogVisitor second; + private final BiFunction combiner; + + CombineWithLogVisitor(LogVisitor first, LogVisitor second, BiFunction combiner) { + this.first = first; + this.second = second; + this.combiner = combiner; + } + + @Override + public Optional serviceV1(ServiceLogV1 serviceLogV1) { + return combineWith(serviceLogV1, LogVisitor::serviceV1); + } + + @Override + public Optional requestV2(RequestLogV2 requestLogV2) { + return combineWith(requestLogV2, LogVisitor::requestV2); + } + + @Override + public Optional eventV2(EventLogV2 eventLogV2) { + return combineWith(eventLogV2, LogVisitor::eventV2); + } + + @Override + public Optional metricV1(MetricLogV1 metricLogV1) { + return combineWith(metricLogV1, LogVisitor::metricV1); + } + + @Override + public Optional traceV1(TraceLogV1 traceLogV1) { + return combineWith(traceLogV1, LogVisitor::traceV1); + } + + @Override + public Optional auditV2(AuditLogV2 auditLogV2) { + return combineWith(auditLogV2, LogVisitor::auditV2); + } + + @Override + public Optional diagnosticV1(DiagnosticLogV1 diagnosticLogV1) { + return combineWith(diagnosticLogV1, LogVisitor::diagnosticV1); + } + + private Optional combineWith(L log, LogFunctionSelector logFunc) { + return logFunc.apply(first, log).flatMap(t -> { + return logFunc.apply(second, log).map(u -> combiner.apply(t, u)); + }); + } + + private interface LogFunctionSelector { + Optional apply(LogVisitor logVisitor, L logLine); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/EventLogFormatter.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/EventLogFormatter.java new file mode 100644 index 00000000..4cd5ce62 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/EventLogFormatter.java @@ -0,0 +1,39 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.palantir.witchcraft.api.logging.EventLogV2; +import java.time.format.DateTimeFormatter; + +final class EventLogFormatter { + private EventLogFormatter() {} + + static String format(EventLogV2 event) { + StringBuilder buffer = new StringBuilder().append('['); + DateTimeFormatter.ISO_INSTANT.formatTo(event.getTime(), buffer); + buffer.append("] ").append(event.getEventName()); + if (!event.getValues().isEmpty()) { + buffer.append(' '); + Formatting.niceMap(event.getValues(), buffer); + } + if (!event.getUnsafeParams().isEmpty()) { + buffer.append(' '); + Formatting.niceMap(event.getUnsafeParams(), buffer); + } + return buffer.toString(); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/Formatting.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/Formatting.java new file mode 100644 index 00000000..16d60bbc --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/Formatting.java @@ -0,0 +1,72 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.google.common.base.CharMatcher; +import java.util.Map; +import java.util.function.Consumer; + +/** Utility functionality shared between {@link LogFormatter} implementations. */ +final class Formatting { + + static final CharMatcher NEWLINE_MATCHER = CharMatcher.is('\n'); + + private static final ThreadLocal REUSABLE_STRING_BUILDER = + ThreadLocal.withInitial(() -> new StringBuilder(1024)); + + static void niceMap(Map params, StringBuilder sb) { + sb.append('('); + formatParamsTo(params, sb); + if (!params.isEmpty()) { + sb.setLength(sb.length() - 2); + } + sb.append(')'); + } + + static void formatParamsTo(Map params, StringBuilder sb) { + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + sb.append(key).append(": ").append(safeString(value)).append(", "); + } + } + + static String safeString(Object value) { + try { + return String.valueOf(value); + } catch (RuntimeException e) { + // Fallback if toString throws + return value.getClass().getSimpleName() + '@' + System.identityHashCode(value); + } + } + + static String withStringBuilder(Consumer function) { + StringBuilder builder = REUSABLE_STRING_BUILDER.get(); + builder.setLength(0); + function.accept(builder); + String result = builder.toString(); + if (builder.length() > 1024 * 16) { + // Buffer has grown too large, allow the instance to be collected + REUSABLE_STRING_BUILDER.remove(); + } else { + builder.setLength(0); + } + return result; + } + + private Formatting() {} +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogFormatter.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogFormatter.java new file mode 100644 index 00000000..38bdc643 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogFormatter.java @@ -0,0 +1,53 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.palantir.witchcraft.api.logging.EventLogV2; +import com.palantir.witchcraft.api.logging.MetricLogV1; +import com.palantir.witchcraft.api.logging.RequestLogV2; +import com.palantir.witchcraft.api.logging.ServiceLogV1; +import com.palantir.witchcraft.api.logging.TraceLogV1; +import java.util.Optional; + +public enum LogFormatter implements LogVisitor { + INSTANCE; + + @Override + public Optional serviceV1(ServiceLogV1 serviceLogV1) { + return Optional.of(ServiceLogFormatter.format(serviceLogV1)); + } + + @Override + public Optional requestV2(RequestLogV2 requestLogV2) { + return Optional.of(RequestLogFormatter.format(requestLogV2)); + } + + @Override + public Optional eventV2(EventLogV2 eventLogV2) { + return Optional.of(EventLogFormatter.format(eventLogV2)); + } + + @Override + public Optional metricV1(MetricLogV1 metricLogV1) { + return Optional.of(MetricLogFormatter.format(metricLogV1)); + } + + @Override + public Optional traceV1(TraceLogV1 traceLogV1) { + return Optional.of(TraceLogFormatter.format(traceLogV1)); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogParser.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogParser.java new file mode 100644 index 00000000..bea68855 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogParser.java @@ -0,0 +1,123 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.common.collect.ImmutableList; +import com.palantir.witchcraft.api.logging.AuditLogV2; +import com.palantir.witchcraft.api.logging.DiagnosticLogV1; +import com.palantir.witchcraft.api.logging.EventLogV2; +import com.palantir.witchcraft.api.logging.MetricLogV1; +import com.palantir.witchcraft.api.logging.RequestLogV2; +import com.palantir.witchcraft.api.logging.ServiceLogV1; +import com.palantir.witchcraft.api.logging.TraceLogV1; +import com.palantir.witchcraft.api.logging.WrappedLogV1; +import java.util.Optional; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings({"PreferSafeLogger", "Slf4jLogsafeArgs"}) // Logging for the IDE +public final class LogParser { + private static final Logger log = LoggerFactory.getLogger(LogParser.class); + + private static final String SERVICE_V1 = "service.1"; + private static final String REQUEST_V2 = "request.2"; + private static final String EVENT_V2 = "event.2"; + private static final String METRIC_V1 = "metric.1"; + private static final String TRACE_V1 = "trace.1"; + private static final String AUDIT_V2 = "audit.2"; + private static final String DIAGNOSTIC_V1 = "diagnostic.1"; + private static final String WRAPPED_V1 = "wrapped.1"; + private static final ImmutableList LOG_TYPES = ImmutableList.of( + SERVICE_V1, REQUEST_V2, EVENT_V2, METRIC_V1, TRACE_V1, AUDIT_V2, DIAGNOSTIC_V1, WRAPPED_V1); + + private static final String WITCHCRAFT_LOG_PATTERN_STRING = "\\{.*?\"type\"\\s*?:\\s*?\"(" + + LOG_TYPES.stream().map(Pattern::quote).collect(Collectors.joining("|")) + ")\".*?}"; + private static final Pattern WITCHCRAFT_LOG_PATTERN = Pattern.compile(WITCHCRAFT_LOG_PATTERN_STRING); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .registerModule(new Jdk8Module().configureAbsentsAsNulls(true)) + .registerModule(new JavaTimeModule()) + .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .disable(DeserializationFeature.WRAP_EXCEPTIONS); + + private final LogVisitor logVisitor; + private final WrappedLogDelegatingVisitor wrappedLogDelegatingVisitor; + + public LogParser(LogVisitor logVisitor) { + this.logVisitor = logVisitor; + this.wrappedLogDelegatingVisitor = new WrappedLogDelegatingVisitor<>(logVisitor); + } + + public static boolean anyPossibleWitchcraftLogsInBlock(String blockOfText) { + return WITCHCRAFT_LOG_PATTERN.matcher(blockOfText).find(); + } + + public Optional tryParse(String logLine) { + Matcher matcher = WITCHCRAFT_LOG_PATTERN.matcher(logLine); + + if (!matcher.matches()) { + return Optional.empty(); + } + + String logType = matcher.group(1); + + switch (logType) { + case SERVICE_V1: + return applyToLogLine(logLine, ServiceLogV1.class, logVisitor::serviceV1); + case REQUEST_V2: + return applyToLogLine(logLine, RequestLogV2.class, logVisitor::requestV2); + case EVENT_V2: + return applyToLogLine(logLine, EventLogV2.class, logVisitor::eventV2); + case METRIC_V1: + return applyToLogLine(logLine, MetricLogV1.class, logVisitor::metricV1); + case TRACE_V1: + return applyToLogLine(logLine, TraceLogV1.class, logVisitor::traceV1); + case AUDIT_V2: + return applyToLogLine(logLine, AuditLogV2.class, logVisitor::auditV2); + case DIAGNOSTIC_V1: + return applyToLogLine(logLine, DiagnosticLogV1.class, logVisitor::diagnosticV1); + case WRAPPED_V1: + return applyToLogLine(logLine, WrappedLogV1.class, wrappedLogV1 -> wrappedLogV1 + .getPayload() + .accept(wrappedLogDelegatingVisitor)); + } + + return Optional.empty(); + } + + private Optional applyToLogLine(String logLine, Class clazz, Function> function) { + return parseJson(logLine, clazz).flatMap(function); + } + + private static Optional parseJson(String logLine, Class clazz) { + try { + return Optional.of(OBJECT_MAPPER.readValue(logLine, clazz)); + } catch (JsonProcessingException | RuntimeException e) { + log.warn("Failed to deserialize witchcraft event for line '{}' into type {}", logLine, clazz, e); + return Optional.empty(); + } + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogVisitor.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogVisitor.java new file mode 100644 index 00000000..dd5cbb60 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogVisitor.java @@ -0,0 +1,100 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.palantir.witchcraft.api.logging.AuditLogV2; +import com.palantir.witchcraft.api.logging.DiagnosticLogV1; +import com.palantir.witchcraft.api.logging.EventLogV2; +import com.palantir.witchcraft.api.logging.MetricLogV1; +import com.palantir.witchcraft.api.logging.RequestLogV2; +import com.palantir.witchcraft.api.logging.ServiceLogV1; +import com.palantir.witchcraft.api.logging.TraceLogV1; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +public interface LogVisitor { + default Optional serviceV1(ServiceLogV1 _serviceLogV1) { + return defaultValue(); + } + + default Optional requestV2(RequestLogV2 _requestLogV2) { + return defaultValue(); + } + + default Optional eventV2(EventLogV2 _eventLogV2) { + return defaultValue(); + } + + default Optional metricV1(MetricLogV1 _metricLogV1) { + return defaultValue(); + } + + default Optional traceV1(TraceLogV1 _traceLogV1) { + return defaultValue(); + } + + default Optional auditV2(AuditLogV2 _auditLogV2) { + return defaultValue(); + } + + default Optional diagnosticV1(DiagnosticLogV1 _diagnosticLogV1) { + return defaultValue(); + } + + default Optional defaultValue() { + return Optional.empty(); + } + + /** + * Combine this {@link LogVisitor} with another {@link LogVisitor} to produce a composite short-circuiting + * {@link LogVisitor}. + * + *

If the {@code otherLogVisitor} doesn't return a value, then this composite doesn't return a + * value either. + * + *

For each log given to this composite, this log visitor is visited and if it returns a value the second log + * visitor is visited, and then the value from each is combined using the given function. + */ + default LogVisitor combineWith(LogVisitor otherLogVisitor, BiFunction combiner) { + return new CombineWithLogVisitor<>(this, otherLogVisitor, combiner); + } + + /** + * Combine this {@link LogVisitor} with another {@link LogVisitor} to produce a composite {@link LogVisitor}. + * + *

If the {@code otherLogVisitor} doesn't return a value, then this composite behaves the same as the + * original visitor. + * + *

For each log given to this composite, this log visitor is visited and if it returns a value the second log + * visitor is visited, and then the value from each is handed off to the given {@code effect} consumer. + */ + default LogVisitor combineWithEffect(LogVisitor otherLogVisitor, BiConsumer effect) { + return new CombineWithLogVisitor<>(this, LogVisitors.liftOptional(otherLogVisitor), (original, maybeOther) -> { + maybeOther.ifPresent(other -> effect.accept(original, other)); + return original; + }); + } + + /** + * Produce a {@link LogVisitor} which for every log type just returns a value from the supplier. + */ + static LogVisitor fromSupplier(Supplier supplier) { + return new SupplierLogVisitor<>(supplier); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogVisitors.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogVisitors.java new file mode 100644 index 00000000..e8aab313 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/LogVisitors.java @@ -0,0 +1,40 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import java.lang.reflect.Proxy; +import java.util.Optional; + +final class LogVisitors { + + /** + * A log visitor that always returns something, though that something is now an Optional indicating + * whether the given {@code visitor} returned something or not. + */ + @SuppressWarnings("unchecked") + static LogVisitor> liftOptional(LogVisitor visitor) { + return (LogVisitor>) Proxy.newProxyInstance( + visitor.getClass().getClassLoader(), new Class[] {LogVisitor.class}, (_proxy, method, args) -> { + if (method.getReturnType() == Optional.class) { + return Optional.of(method.invoke(visitor, args)); + } + return method.invoke(visitor, args); + }); + } + + private LogVisitors() {} +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/MetricLogFormatter.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/MetricLogFormatter.java new file mode 100644 index 00000000..099ec5f3 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/MetricLogFormatter.java @@ -0,0 +1,47 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.palantir.witchcraft.api.logging.MetricLogV1; +import java.time.format.DateTimeFormatter; + +final class MetricLogFormatter { + private MetricLogFormatter() {} + + static String format(MetricLogV1 metric) { + return Formatting.withStringBuilder(buffer -> { + buffer.append('['); + DateTimeFormatter.ISO_INSTANT.formatTo(metric.getTime(), buffer); + buffer.append("] METRIC ") + .append(metric.getMetricName()) + .append(' ') + .append(metric.getMetricType()); + if (!metric.getValues().isEmpty()) { + buffer.append(' '); + Formatting.niceMap(metric.getValues(), buffer); + } + if (!metric.getTags().isEmpty()) { + buffer.append(' '); + Formatting.niceMap(metric.getTags(), buffer); + } + if (!metric.getUnsafeParams().isEmpty()) { + buffer.append(' '); + Formatting.niceMap(metric.getUnsafeParams(), buffer); + } + }); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/RequestLogFormatter.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/RequestLogFormatter.java new file mode 100644 index 00000000..1f7bf0cb --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/RequestLogFormatter.java @@ -0,0 +1,60 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.palantir.witchcraft.api.logging.RequestLogV2; +import java.time.format.DateTimeFormatter; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class RequestLogFormatter { + private RequestLogFormatter() {} + + private static final Pattern REQUEST_PARAMETER_PATTERN = Pattern.compile("\\{(\\S+?)}"); + + static String format(RequestLogV2 request) { + return Formatting.withStringBuilder(buffer -> { + buffer.append('['); + DateTimeFormatter.ISO_INSTANT.formatTo(request.getTime(), buffer); + buffer.append("] \""); + request.getMethod().ifPresent(method -> buffer.append(method).append(' ')); + buffer.append(getPathWithParameters(request)) + .append(' ') + .append(request.getProtocol()) + .append("\" ") + .append(request.getStatus()) + .append(' ') + .append(request.getResponseSize().longValue()) + .append(' ') + .append(request.getDuration().longValue()); + }); + } + + private static String getPathWithParameters(RequestLogV2 request) { + String path = request.getPath(); + Matcher matcher = REQUEST_PARAMETER_PATTERN.matcher(path); + while (matcher.find()) { + String name = matcher.group(1); + Object value = request.getParams() + .getOrDefault(name, request.getUnsafeParams().get(name)); + if (value != null) { + path = path.replace("{" + name + "}", Formatting.safeString(value)); + } + } + return path; + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/ServiceLogFormatter.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/ServiceLogFormatter.java new file mode 100644 index 00000000..da852431 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/ServiceLogFormatter.java @@ -0,0 +1,80 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.google.common.base.Strings; +import com.palantir.witchcraft.api.logging.ServiceLogV1; +import java.time.format.DateTimeFormatter; +import org.slf4j.helpers.MessageFormatter; + +final class ServiceLogFormatter { + private ServiceLogFormatter() {} + + static String format(ServiceLogV1 service) { + return Formatting.withStringBuilder(buffer -> { + buffer.append(service.getLevel()); + while (buffer.length() < 6) { + buffer.append(' '); + } + buffer.append('['); + DateTimeFormatter.ISO_INSTANT.formatTo(service.getTime(), buffer); + buffer.append("] ") + .append(service.getOrigin().orElse("")) + .append(": ") + .append(getMessage(service)); + if (!service.getParams().isEmpty() || !service.getUnsafeParams().isEmpty()) { + buffer.append(" ("); + Formatting.formatParamsTo(service.getParams(), buffer); + Formatting.formatParamsTo(service.getUnsafeParams(), buffer); + // Reset trailing separator + buffer.setLength(buffer.length() - 2); + buffer.append(')'); + } + service.getStacktrace() + .map(input -> Strings.emptyToNull(Formatting.NEWLINE_MATCHER.trimFrom(input))) + .ifPresent(stackTrace -> buffer.append('\n').append(stackTrace)); + }); + } + + private static String getMessage(ServiceLogV1 service) { + String formatString = service.getMessage(); + // If placeholders are found, attempt slf4j-style interpolation + int placeholders = countPlaceholders(formatString); + if (placeholders > 0) { + Object[] parameters = new Object[placeholders]; + for (int i = 0; i < placeholders; i++) { + parameters[i] = service.getUnsafeParams() + // Use the placeholder string by default to avoid modifying non-existent parameters. + // This can occur if only some parameters are wrapped with log-safe args. + .getOrDefault("" + i, "{}"); + } + // Use the slf4j provided utility directly + return MessageFormatter.arrayFormat(formatString, parameters).getMessage(); + } + return formatString; + } + + private static int countPlaceholders(String formatString) { + int count = 0; + for (int i = 1; i < formatString.length(); i++) { + if (formatString.charAt(i - 1) == '{' && formatString.charAt(i) == '}') { + count++; + } + } + return count; + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/SupplierLogVisitor.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/SupplierLogVisitor.java new file mode 100644 index 00000000..597b2a29 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/SupplierLogVisitor.java @@ -0,0 +1,74 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.palantir.witchcraft.api.logging.AuditLogV2; +import com.palantir.witchcraft.api.logging.DiagnosticLogV1; +import com.palantir.witchcraft.api.logging.EventLogV2; +import com.palantir.witchcraft.api.logging.MetricLogV1; +import com.palantir.witchcraft.api.logging.RequestLogV2; +import com.palantir.witchcraft.api.logging.ServiceLogV1; +import com.palantir.witchcraft.api.logging.TraceLogV1; +import java.util.Optional; +import java.util.function.Supplier; + +final class SupplierLogVisitor implements LogVisitor { + private final Supplier supplier; + + SupplierLogVisitor(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public Optional serviceV1(ServiceLogV1 _serviceLogV1) { + return get(); + } + + @Override + public Optional requestV2(RequestLogV2 _requestLogV2) { + return get(); + } + + @Override + public Optional eventV2(EventLogV2 _eventLogV2) { + return get(); + } + + @Override + public Optional metricV1(MetricLogV1 _metricLogV1) { + return get(); + } + + @Override + public Optional traceV1(TraceLogV1 _traceLogV1) { + return get(); + } + + @Override + public Optional auditV2(AuditLogV2 _auditLogV2) { + return get(); + } + + @Override + public Optional diagnosticV1(DiagnosticLogV1 _diagnosticLogV1) { + return get(); + } + + private Optional get() { + return Optional.of(supplier.get()); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/TraceLogFormatter.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/TraceLogFormatter.java new file mode 100644 index 00000000..bdc41481 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/TraceLogFormatter.java @@ -0,0 +1,40 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.palantir.witchcraft.api.logging.TraceLogV1; +import java.time.format.DateTimeFormatter; + +final class TraceLogFormatter { + private TraceLogFormatter() {} + + static String format(TraceLogV1 trace) { + return Formatting.withStringBuilder(buffer -> { + buffer.append('['); + DateTimeFormatter.ISO_INSTANT.formatTo(trace.getTime(), buffer); + buffer.append("] traceId: ") + .append(trace.getSpan().getTraceId()) + .append(" id: ") + .append(trace.getSpan().getId()) + .append(" name: ") + .append(trace.getSpan().getName()) + .append(" duration: ") + .append(trace.getSpan().getDuration().longValue()) + .append(" microseconds"); + }); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/WrappedLogDelegatingVisitor.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/WrappedLogDelegatingVisitor.java new file mode 100644 index 00000000..05c48422 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/format/WrappedLogDelegatingVisitor.java @@ -0,0 +1,75 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import com.palantir.witchcraft.api.logging.AuditLogV2; +import com.palantir.witchcraft.api.logging.DiagnosticLogV1; +import com.palantir.witchcraft.api.logging.EventLogV2; +import com.palantir.witchcraft.api.logging.MetricLogV1; +import com.palantir.witchcraft.api.logging.RequestLogV2; +import com.palantir.witchcraft.api.logging.ServiceLogV1; +import com.palantir.witchcraft.api.logging.TraceLogV1; +import com.palantir.witchcraft.api.logging.WrappedLogV1Payload; +import java.util.Optional; + +final class WrappedLogDelegatingVisitor implements WrappedLogV1Payload.Visitor> { + private final LogVisitor logVisitor; + + WrappedLogDelegatingVisitor(LogVisitor logVisitor) { + this.logVisitor = logVisitor; + } + + @Override + public Optional visitServiceLogV1(ServiceLogV1 value) { + return logVisitor.serviceV1(value); + } + + @Override + public Optional visitRequestLogV2(RequestLogV2 value) { + return logVisitor.requestV2(value); + } + + @Override + public Optional visitTraceLogV1(TraceLogV1 value) { + return logVisitor.traceV1(value); + } + + @Override + public Optional visitEventLogV2(EventLogV2 value) { + return logVisitor.eventV2(value); + } + + @Override + public Optional visitMetricLogV1(MetricLogV1 value) { + return logVisitor.metricV1(value); + } + + @Override + public Optional visitAuditLogV2(AuditLogV2 value) { + return logVisitor.auditV2(value); + } + + @Override + public Optional visitDiagnosticLogV1(DiagnosticLogV1 value) { + return logVisitor.diagnosticV1(value); + } + + @Override + public Optional visitUnknown(String _unknownType) { + return Optional.empty(); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/ConsoleViewCalculator.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/ConsoleViewCalculator.java new file mode 100644 index 00000000..7a182fd2 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/ConsoleViewCalculator.java @@ -0,0 +1,56 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.intellij.execution.ui.ConsoleViewContentType; +import com.palantir.witchcraft.api.logging.EventLogV2; +import com.palantir.witchcraft.api.logging.MetricLogV1; +import com.palantir.witchcraft.api.logging.RequestLogV2; +import com.palantir.witchcraft.api.logging.ServiceLogV1; +import com.palantir.witchcraft.api.logging.TraceLogV1; +import com.palantir.witchcraft.logging.format.LogVisitor; +import java.util.Optional; + +enum ConsoleViewCalculator implements LogVisitor { + INSTANCE; + + @Override + public Optional serviceV1(ServiceLogV1 serviceLogV1) { + return Optional.of(WitchcraftConsoleViewContentTypes.SERVICE_TYPES.getOrDefault( + serviceLogV1.getLevel(), WitchcraftConsoleViewContentTypes.DEFAULT_TYPE)); + } + + @Override + public Optional requestV2(RequestLogV2 _requestLogV2) { + return Optional.of(WitchcraftConsoleViewContentTypes.REQUEST_TYPE); + } + + @Override + public Optional eventV2(EventLogV2 _eventLogV2) { + return Optional.of(WitchcraftConsoleViewContentTypes.EVENT_TYPE); + } + + @Override + public Optional metricV1(MetricLogV1 _metricLogV1) { + return Optional.of(WitchcraftConsoleViewContentTypes.METRIC_TYPE); + } + + @Override + public Optional traceV1(TraceLogV1 _traceLogV1) { + return Optional.of(WitchcraftConsoleViewContentTypes.TRACE_TYPE); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftConsoleViewContentTypes.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftConsoleViewContentTypes.java new file mode 100644 index 00000000..95caac0c --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftConsoleViewContentTypes.java @@ -0,0 +1,64 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.google.common.collect.ImmutableMap; +import com.intellij.execution.ui.ConsoleViewContentType; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.util.Pair; +import com.palantir.witchcraft.api.logging.LogLevel; +import java.awt.Color; +import java.awt.Font; + +/** {@link ConsoleViewContentType} definitions used by this plugin. */ +final class WitchcraftConsoleViewContentTypes { + + static final ConsoleViewContentType DEFAULT_TYPE = + createViewType("WITCHCRAFT_DEFAULT", ConsoleViewContentType.NORMAL_OUTPUT_KEY); + static final ImmutableMap SERVICE_TYPES = + ImmutableMap.builder() + .put(LogLevel.FATAL, createViewType("WITCHCRAFT_SERVICE_FATAL", Color.RED)) + .put(LogLevel.ERROR, createViewType("WITCHCRAFT_SERVICE_ERROR", Color.RED)) + .put(LogLevel.WARN, createViewType("WITCHCRAFT_SERVICE_WARN", Color.YELLOW)) + .put( + LogLevel.INFO, + createViewType("WITCHCRAFT_SERVICE_INFO", ConsoleViewContentType.NORMAL_OUTPUT_KEY)) + .put(LogLevel.DEBUG, createViewType("WITCHCRAFT_SERVICE_DEBUG", Color.CYAN)) + .put(LogLevel.TRACE, createViewType("WITCHCRAFT_SERVICE_TRACE", Color.CYAN)) + .build(); + static final ConsoleViewContentType EVENT_TYPE = createViewType("WITCHCRAFT_EVENT", Color.WHITE); + static final ConsoleViewContentType METRIC_TYPE = createViewType("WITCHCRAFT_METRIC", Color.DARK_GRAY); + static final ConsoleViewContentType REQUEST_TYPE = createViewType("WITCHCRAFT_REQUEST", Color.WHITE); + static final ConsoleViewContentType TRACE_TYPE = createViewType("WITCHCRAFT_TRACE", Color.WHITE); + // Marker content type for newlines applied by the formatter + static final ConsoleViewContentType NEWLINE_TYPE = createViewType("WITCHCRAFT_NEWLINE", Color.DARK_GRAY); + static final Pair NEWLINE = Pair.create("\n", NEWLINE_TYPE); + + private WitchcraftConsoleViewContentTypes() {} + + private static ConsoleViewContentType createViewType(String name, Color color) { + return createViewType( + name, + TextAttributesKey.createTempTextAttributesKey( + name, new TextAttributes(color, null, null, null, Font.PLAIN))); + } + + private static ConsoleViewContentType createViewType(String name, TextAttributesKey fallback) { + return new ConsoleViewContentType(name, TextAttributesKey.createTextAttributesKey(name, fallback)); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFilter.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFilter.java new file mode 100644 index 00000000..1218e4b8 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFilter.java @@ -0,0 +1,90 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.intellij.execution.filters.InputFilter; +import com.intellij.execution.ui.ConsoleViewContentType; +import com.intellij.openapi.util.Pair; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class WitchcraftLogFilter implements InputFilter { + + private final InputFilter delegate; + private final ImmutableMap displayFilter; + + WitchcraftLogFilter(InputFilter delegate, Supplier settings) { + this.delegate = delegate; + this.displayFilter = ImmutableMap.of( + WitchcraftConsoleViewContentTypes.EVENT_TYPE, + () -> settings.get().getShowEventLogs(), + WitchcraftConsoleViewContentTypes.METRIC_TYPE, + () -> settings.get().getShowMetricLogs(), + WitchcraftConsoleViewContentTypes.REQUEST_TYPE, + () -> settings.get().getShowRequestLogs(), + WitchcraftConsoleViewContentTypes.TRACE_TYPE, + () -> settings.get().getShowTraceLogs()); + } + + @Nullable + @Override + public List> applyFilter( + @NotNull String text, @NotNull ConsoleViewContentType contentType) { + List> delegateResult = delegate.applyFilter(text, contentType); + if (delegateResult == null || !containsWitchcraftData(delegateResult)) { + return delegateResult; + } + // When multiple lines are returned, the result may include a Pair representing a newline after a parsed + // Witchcraft line. If we filter an event, we must also filter the trailing newline character. + boolean removeNextLineIfNewline = false; + ImmutableList.Builder> result = + ImmutableList.builderWithExpectedSize(delegateResult.size()); + for (Pair item : delegateResult) { + if (removeNextLineIfNewline) { + removeNextLineIfNewline = false; + if (WitchcraftConsoleViewContentTypes.NEWLINE.equals(item)) { + continue; + } + } + if (displayFilter.getOrDefault(item.getSecond(), () -> true).getAsBoolean()) { + result.add(item); + } else { + // When results are filtered, we must also remove the associated newline. + removeNextLineIfNewline = true; + } + } + return result.build(); + } + + private static boolean containsWitchcraftData(List> lines) { + for (Pair item : lines) { + // The null check is likely unnecessarily defensive, the goal is to avoid breaking any non-witchcraft + // application, which would degrade trust in the plugin. I'm less worried about breaking witchcraft + // applications because there's a direct relationship to the plugin, and would be less frustrating + // to debug. + if (item != null && item.getSecond().toString().startsWith("WITCHCRAFT_")) { + return true; + } + } + return false; + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatFilterProvider.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatFilterProvider.java new file mode 100644 index 00000000..0ef26b0b --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatFilterProvider.java @@ -0,0 +1,33 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.intellij.execution.filters.ConsoleInputFilterProvider; +import com.intellij.execution.filters.InputFilter; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +/** Intellij plugin implementation. */ +public final class WitchcraftLogFormatFilterProvider implements ConsoleInputFilterProvider { + @NotNull + @Override + public InputFilter[] getDefaultFilters(@NotNull Project project) { + WitchcraftLogSettingsManager settingsManager = WitchcraftLogSettingsManager.getInstance(project); + return new InputFilter[] {new WitchcraftLogFilter(WitchcraftLogFormatter.INSTANCE, settingsManager::getSettings) + }; + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatter.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatter.java new file mode 100644 index 00000000..548f67a2 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatter.java @@ -0,0 +1,74 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.intellij.execution.filters.InputFilter; +import com.intellij.execution.ui.ConsoleViewContentType; +import com.intellij.openapi.util.Pair; +import com.palantir.witchcraft.logging.format.LogFormatter; +import com.palantir.witchcraft.logging.format.LogParser; +import java.util.Iterator; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +enum WitchcraftLogFormatter implements InputFilter { + INSTANCE; + + private static final LogParser> LOG_PARSER = + new LogParser<>(LogFormatter.INSTANCE.combineWith(ConsoleViewCalculator.INSTANCE, Pair::createNonNull)); + + private static final Splitter SPLITTER = Splitter.on('\n'); + + @Override + public ImmutableList> applyFilter( + @NotNull String text, @NotNull ConsoleViewContentType contentType) { + if (!LogParser.anyPossibleWitchcraftLogsInBlock(text)) { + // Return fast if there are no witchcraft logs + return null; + } + + boolean witchcraftLogParsed = false; + ImmutableList.Builder> result = ImmutableList.builder(); + + Iterator lineIterator = SPLITTER.split(text).iterator(); + while (lineIterator.hasNext()) { + String line = lineIterator.next(); + Optional> maybeParsed = LOG_PARSER.tryParse(line); + + if (maybeParsed.isPresent()) { + witchcraftLogParsed = true; + result.add(maybeParsed.get()); + } else { + result.add(Pair.createNonNull(line, contentType)); + } + + // If this is not the last piece, add a newline + if (lineIterator.hasNext()) { + result.add(WitchcraftConsoleViewContentTypes.NEWLINE); + } + } + + if (!witchcraftLogParsed) { + // When nothing has been decoded, return null based on InputFilter.applyFilter documentation + return null; + } + + return result.build(); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettings.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettings.java new file mode 100644 index 00000000..1268fd23 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettings.java @@ -0,0 +1,65 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.intellij.util.xmlb.annotations.OptionTag; +import com.intellij.util.xmlb.annotations.Tag; +import org.immutables.value.Value; + +/** + * Project-scope settings bean containing persistent state for the plugin. + * A mutable implementation with XML annotations is generated to support integration with + * {@linkplain com.intellij.util.xmlb.XmlSerializer}. + */ +@Value.Immutable +@Value.Modifiable +@WitchcraftLogSettingsStyle +@Tag("WitchcraftLogSettings") +interface WitchcraftLogSettings { + + @OptionTag + @Value.Default + default boolean getShowEventLogs() { + return true; + } + + @OptionTag + @Value.Default + default boolean getShowMetricLogs() { + return false; + } + + @OptionTag + @Value.Default + default boolean getShowRequestLogs() { + return true; + } + + @OptionTag + @Value.Default + default boolean getShowTraceLogs() { + return true; + } + + static Builder builder() { + return new Builder(); + } + + final class Builder extends ImmutableWitchcraftLogSettings.Builder { + Builder() {} + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsManager.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsManager.java new file mode 100644 index 00000000..65f25b22 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsManager.java @@ -0,0 +1,30 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.intellij.openapi.project.Project; + +interface WitchcraftLogSettingsManager { + + WitchcraftLogSettings getSettings(); + + void setSettings(WitchcraftLogSettings newSettings); + + static WitchcraftLogSettingsManager getInstance(Project project) { + return project.getService(WitchcraftLogSettingsManager.class); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsManagerImpl.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsManagerImpl.java new file mode 100644 index 00000000..4c666e35 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsManagerImpl.java @@ -0,0 +1,49 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import org.jetbrains.annotations.NotNull; + +@State(name = "WitchcraftLogSettings") +final class WitchcraftLogSettingsManagerImpl + implements WitchcraftLogSettingsManager, PersistentStateComponent { + private static final WitchcraftLogSettings DEFAULT_SETTINGS = + WitchcraftLogSettings.builder().build(); + private volatile WitchcraftLogSettings settings = DEFAULT_SETTINGS; + + @Override + public WitchcraftLogSettings getSettings() { + return settings; + } + + @Override + public void setSettings(WitchcraftLogSettings newSettings) { + this.settings = newSettings; + } + + @Override + public ModifiableWitchcraftLogSettings getState() { + return ModifiableWitchcraftLogSettings.create().from(settings); + } + + @Override + public void loadState(@NotNull ModifiableWitchcraftLogSettings state) { + this.settings = WitchcraftLogSettings.builder().from(state).build(); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsPanel.form b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsPanel.form new file mode 100644 index 00000000..d4b9aa9a --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsPanel.form @@ -0,0 +1,50 @@ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsPanel.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsPanel.java new file mode 100644 index 00000000..c8fdd423 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsPanel.java @@ -0,0 +1,81 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.project.Project; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JPanel; +import org.jetbrains.annotations.Nls; + +public final class WitchcraftLogSettingsPanel implements Configurable { + private JCheckBox showEventLogsCheckBox; + private JCheckBox showRequestLogsCheckBox; + private JCheckBox showTraceLogsCheckBox; + private JCheckBox showMetricLogsCheckBox; + private JPanel panel; + + private final WitchcraftLogSettingsManager settingsManager; + + public WitchcraftLogSettingsPanel(Project project) { + this(WitchcraftLogSettingsManager.getInstance(project)); + } + + public WitchcraftLogSettingsPanel(WitchcraftLogSettingsManager settingsManager) { + this.settingsManager = settingsManager; + } + + @Nls(capitalization = Nls.Capitalization.Title) + @Override + public String getDisplayName() { + return "Witchcraft Log Display"; + } + + @Override + public JComponent createComponent() { + return panel; + } + + @Override + public boolean isModified() { + return !deriveSettings().equals(settingsManager.getSettings()); + } + + private WitchcraftLogSettings deriveSettings() { + return WitchcraftLogSettings.builder() + .showEventLogs(showEventLogsCheckBox.isSelected()) + .showRequestLogs(showRequestLogsCheckBox.isSelected()) + .showTraceLogs(showTraceLogsCheckBox.isSelected()) + .showMetricLogs(showMetricLogsCheckBox.isSelected()) + .build(); + } + + @Override + public void apply() { + settingsManager.setSettings(deriveSettings()); + } + + @Override + public void reset() { + WitchcraftLogSettings settings = settingsManager.getSettings(); + showEventLogsCheckBox.setSelected(settings.getShowEventLogs()); + showRequestLogsCheckBox.setSelected(settings.getShowRequestLogs()); + showTraceLogsCheckBox.setSelected(settings.getShowTraceLogs()); + showMetricLogsCheckBox.setSelected(settings.getShowMetricLogs()); + } +} diff --git a/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsStyle.java b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsStyle.java new file mode 100644 index 00000000..4b4b72a1 --- /dev/null +++ b/witchcraft-logging-idea/src/main/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsStyle.java @@ -0,0 +1,30 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import com.intellij.util.xmlb.annotations.OptionTag; +import com.intellij.util.xmlb.annotations.Tag; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.immutables.value.Value; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +@Value.Style(passAnnotations = {OptionTag.class, Tag.class}) +@interface WitchcraftLogSettingsStyle {} diff --git a/witchcraft-logging-idea/src/main/resources/META-INF/plugin.xml b/witchcraft-logging-idea/src/main/resources/META-INF/plugin.xml new file mode 100644 index 00000000..ae3444d4 --- /dev/null +++ b/witchcraft-logging-idea/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,16 @@ + + com.palantir.witchcraft.api.logging.idea + Witchcraft Logging + 0.0.0 + Palantir Technologies, Inc. + com.intellij.modules.platform + + + + + + diff --git a/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/EventLogFormatterTest.java b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/EventLogFormatterTest.java new file mode 100644 index 00000000..8aa3b192 --- /dev/null +++ b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/EventLogFormatterTest.java @@ -0,0 +1,38 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.witchcraft.api.logging.EventLogV2; +import org.junit.jupiter.api.Test; + +class EventLogFormatterTest { + @Test + void formats_correctly() { + String formatted = EventLogFormatter.format(EventLogV2.builder() + .type("event.2") + .time(TestData.XMAS_2019) + .eventName("event-name") + .values("value1", 1) + .tags("tag", "foo") + .unsafeParams("unsafe", "bad") + .build()); + + assertThat(formatted).isEqualTo("[2019-12-25T01:02:03Z] event-name (value1: 1) (unsafe: bad)"); + } +} diff --git a/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/LogParserTest.java b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/LogParserTest.java new file mode 100644 index 00000000..ac642ecc --- /dev/null +++ b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/LogParserTest.java @@ -0,0 +1,147 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class LogParserTest { + private static final String EVENT_JSON = "{\"type\":\"event.2\",\"time\":\"2019-05-24T16:40:21.049Z\"," + + "\"eventName\":\"com.palantir.witchcraft.jvm.crash\"," + + "\"values\":{\"numJvmErrorLogs\":\"1\"},\"unsafeParams\":{},\"tags\":{}}"; + + private static final String SERVICE_JSON = "{\"type\":\"service.1\",\"level\":\"ERROR\"," + + "\"time\":\"2019-05-09T15:32:37.692Z\",\"origin\":\"ROOT\"," + + "\"thread\":\"main\",\"message\":\"test good {}\"," + + "\"params\":{\"good\":\":-)\"},\"unsafeParams\":{},\"tags\":{}}"; + + private static final String REQUEST_JSON = "{\"type\":\"request.2\",\"time\":\"2019-05-24T12:40:36.703-04:00\"," + + "\"method\":\"GET\",\"protocol\":\"HTTP/1.1\",\"path\":\"/api/sleep/{millis}\"," + + "\"params\":{\"host\":\"localhost:8443\",\"connection\":\"Keep-Alive\"," + + "\"accept-encoding\":\"gzip\",\"user-agent\":\"okhttp/3.13.1\"}," + + "\"status\":503,\"requestSize\":0,\"responseSize\":78,\"duration\":1935," + + "\"traceId\":\"ba3200b6eb01999a\",\"unsafeParams\":{\"path\":\"/api/sleep/10\"," + + "\"millis\":\"10\"}}"; + + private static final String METRIC_JSON = "{\"type\": \"metric.1\"," + + "\"time\":\"2019-05-24T16:40:52.162Z\"," + + "\"metricName\":\"jvm.heap\",\"metricType\":\"gauge\",\"values\":{\"size\":66274352}," + + "\"tags\":{\"collection\":\"Metaspace\",\"collector\":\"PS Scavenge\"," + + "\"when\":\"after\"},\"unsafeParams\":{}}"; + + private static final String TRACE_JSON = "{\"type\":\"trace.1\",\"time\":\"2019-05-24T16:40:40.95Z\"," + + "\"unsafeParams\":{},\"span\":{\"traceId\":\"2250486695021e19\",\"id\":\"c11b9a31555b7035\"," + + "\"name\":\"config-reload\",\"timestamp\":1558716040949000,\"duration\":618," + + "\"annotations\":[{\"timestamp\":1558716040949000,\"value\":\"lc\"," + + "\"endpoint\":{\"serviceName\":\"my-service\",\"ipv4\":\"10.193.122.103\"}}]}}"; + + private static final String WRAPPED_SERVICE_JSON = "{\"type\":\"wrapped.1\",\"entityName\":\"foo\"," + + "\"entityVersion\":\"1.2.3\",\"payload\":{\"type\":\"serviceLogV1\",\"serviceLogV1\":" + SERVICE_JSON + + "}}"; + + @SuppressWarnings("unchecked") + private final LogVisitor logVisitor = mock( + LogVisitor.class, invocation -> Optional.of(invocation.getMethod().getName())); + + private final LogParser logParser = new LogParser<>(logVisitor); + + @Test + void fastPredicateTest_nonWitchcraft() { + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock("foobar")).isFalse(); + } + + @Test + void fastPredicateTest_newlineBrokenWitchcraftLog() { + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock("{\n" + METRIC_JSON.substring(1))) + .describedAs("Newlines are illegal in Witchcraft logs, don't attempt to match") + .isFalse(); + } + + @Test + void fastPredicateTest_metric() { + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock(METRIC_JSON)).isTrue(); + } + + @Test + void fastPredicateTest_metricNewUnsupported() { + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock("{\"type\":\"metric.5\"," + + "\"time\":\"2019-05-24T16:40:52.162Z\"," + + "\"metricName\":\"jvm.heap\",\"metricType\":\"gauge\",\"values\":{\"size\":66274352}," + + "\"tags\":{\"collection\":\"Metaspace\",\"collector\":\"PS Scavenge\"," + + "\"when\":\"after\"},\"unsafeParams\":{}}")) + .isFalse(); + } + + @Test + void fastPredicateTest_typeWithSpaces() { + // space after '"type": ', slightly different from the default compact formatting + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock("{\"type\": \"metric.1\"," + + "\"time\":\"2019-05-24T16:40:52.162Z\"," + + "\"metricName\":\"jvm.heap\",\"metricType\":\"gauge\",\"values\":{\"size\":66274352}," + + "\"tags\":{\"collection\":\"Metaspace\",\"collector\":\"PS Scavenge\"," + + "\"when\":\"after\"},\"unsafeParams\":{}}")) + .isTrue(); + } + + @Test + void parse_event_logs() { + assertThat(logParser.tryParse(EVENT_JSON)).hasValue("eventV2"); + } + + @Test + void parse_service_logs() { + assertThat(logParser.tryParse(SERVICE_JSON)).hasValue("serviceV1"); + } + + @Test + void parse_request_logs() { + assertThat(logParser.tryParse(REQUEST_JSON)).hasValue("requestV2"); + } + + @Test + void parse_metric_logs() { + assertThat(logParser.tryParse(METRIC_JSON)).hasValue("metricV1"); + } + + @Test + void parse_trace_logs() { + assertThat(logParser.tryParse(TRACE_JSON)).hasValue("traceV1"); + } + + @Test + void parse_wrapped_logs() { + assertThat(logParser.tryParse(WRAPPED_SERVICE_JSON)).hasValue("serviceV1"); + } + + @Test + void not_parse_partial_witchcraft_logs() { + assertThat(logParser.tryParse(SERVICE_JSON.substring(5))).isEmpty(); + } + + @Test + void not_parse_broken_witchcraft_logs() { + assertThat(logParser.tryParse(SERVICE_JSON.replace("message", "mmmm"))).isEmpty(); + } + + @Test + void not_parse_partial_witchcraft_logs_with_extra_data() { + assertThat(logParser.tryParse("some other stuff " + SERVICE_JSON)).isEmpty(); + } +} diff --git a/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/MetricLogFormatterTest.java b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/MetricLogFormatterTest.java new file mode 100644 index 00000000..0209de0b --- /dev/null +++ b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/MetricLogFormatterTest.java @@ -0,0 +1,39 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.witchcraft.api.logging.MetricLogV1; +import org.junit.jupiter.api.Test; + +class MetricLogFormatterTest { + @Test + void formats_correctly() { + String formatted = MetricLogFormatter.format(MetricLogV1.builder() + .type("metric.1") + .time(TestData.XMAS_2019) + .metricName("name") + .metricType("type") + .values("value", 3) + .tags("tag", "foo") + .unsafeParams("unsafe", "bad") + .build()); + + assertThat(formatted).isEqualTo("[2019-12-25T01:02:03Z] METRIC name type (value: 3) (tag: foo) (unsafe: bad)"); + } +} diff --git a/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/RequestLogFormatterTest.java b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/RequestLogFormatterTest.java new file mode 100644 index 00000000..03c07b06 --- /dev/null +++ b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/RequestLogFormatterTest.java @@ -0,0 +1,43 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.conjure.java.lib.SafeLong; +import com.palantir.witchcraft.api.logging.RequestLogV2; +import org.junit.jupiter.api.Test; + +class RequestLogFormatterTest { + @Test + void formats_correctly() { + String formatted = RequestLogFormatter.format(RequestLogV2.builder() + .type("request.1") + .time(TestData.XMAS_2019) + .protocol("http") + .path("/some/path/{param}") + .status(203) + .requestSize(SafeLong.of(20)) + .responseSize(SafeLong.of(40)) + .duration(SafeLong.of(99)) + .method("GET") + .params("param", "value") + .build()); + + assertThat(formatted).isEqualTo("[2019-12-25T01:02:03Z] \"GET /some/path/value http\" 203 40 99"); + } +} diff --git a/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/ServiceLogFormatterTest.java b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/ServiceLogFormatterTest.java new file mode 100644 index 00000000..0ca99b42 --- /dev/null +++ b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/ServiceLogFormatterTest.java @@ -0,0 +1,60 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.witchcraft.api.logging.LogLevel; +import com.palantir.witchcraft.api.logging.ServiceLogV1; +import org.junit.jupiter.api.Test; + +class ServiceLogFormatterTest { + @Test + void formats_logline_correctly_1() { + String formatted = ServiceLogFormatter.format(ServiceLogV1.builder() + .type("service.1") + .level(LogLevel.INFO) + .time(TestData.XMAS_2019) + .message("message {}") + .origin("com.origin") + .thread("thread-1") + .params("param1", "value1") + .unsafeParams("unsafeParam2", "value2") + .stacktrace("java.lang.Exception: stacktrace") + .build()); + + assertThat(formatted) + .isEqualTo( + "INFO [2019-12-25T01:02:03Z] com.origin: message {} (param1: value1, unsafeParam2: value2)\n" + + "java.lang.Exception: stacktrace"); + } + + @Test + void formats_logline_correctly_2() { + String formatted = ServiceLogFormatter.format(ServiceLogV1.builder() + .type("service.1") + .level(LogLevel.ERROR) + .time(TestData.XMAS_2019) + .message("message {}") + .params("param", "value") + .unsafeParams("0", "inlined") + .build()); + + assertThat(formatted) + .isEqualTo("ERROR [2019-12-25T01:02:03Z] : message inlined (param: value, 0: inlined)"); + } +} diff --git a/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/TestData.java b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/TestData.java new file mode 100644 index 00000000..a173ae6d --- /dev/null +++ b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/TestData.java @@ -0,0 +1,26 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +final class TestData { + public static final OffsetDateTime XMAS_2019 = OffsetDateTime.of(2019, 12, 25, 1, 2, 3, 0, ZoneOffset.UTC); + + private TestData() {} +} diff --git a/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/TraceLogFormatterTest.java b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/TraceLogFormatterTest.java new file mode 100644 index 00000000..f72ffefe --- /dev/null +++ b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/format/TraceLogFormatterTest.java @@ -0,0 +1,46 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.format; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.conjure.java.lib.SafeLong; +import com.palantir.witchcraft.api.logging.Span; +import com.palantir.witchcraft.api.logging.TraceLogV1; +import org.junit.jupiter.api.Test; + +class TraceLogFormatterTest { + @Test + void formats_correctly() { + String formatted = TraceLogFormatter.format(TraceLogV1.builder() + .type("trace.1") + .time(TestData.XMAS_2019) + .span(Span.builder() + .traceId("abdefghijklmno") + .id("id") + .name("name") + .timestamp(SafeLong.of(999)) + .duration(SafeLong.of(31)) + .build()) + .unsafeParams("unsafe", "bad") + .build()); + + assertThat(formatted) + .isEqualTo( + "[2019-12-25T01:02:03Z] traceId: abdefghijklmno id: id name: name duration: 31 microseconds"); + } +} diff --git a/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatFilterTest.java b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatFilterTest.java new file mode 100644 index 00000000..5e0fcf3b --- /dev/null +++ b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/idea/WitchcraftLogFormatFilterTest.java @@ -0,0 +1,216 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.intellij.execution.filters.InputFilter; +import com.intellij.execution.ui.ConsoleViewContentType; +import com.intellij.openapi.util.Pair; +import com.palantir.witchcraft.logging.format.LogParser; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +public final class WitchcraftLogFormatFilterTest { + private static final String EVENT_JSON = "{\"type\":\"event.2\",\"time\":\"2019-05-24T16:40:21.049Z\"," + + "\"eventName\":\"com.palantir.witchcraft.jvm.crash\"," + + "\"values\":{\"numJvmErrorLogs\":\"1\"},\"unsafeParams\":{},\"tags\":{}}"; + private static final String EVENT_FORMATTED = + "[2019-05-24T16:40:21.049Z] com.palantir.witchcraft.jvm.crash " + "(numJvmErrorLogs: 1)"; + + private static final String SERVICE_JSON = "{\"type\":\"service.1\",\"level\":\"ERROR\"," + + "\"time\":\"2019-05-09T15:32:37.692Z\",\"origin\":\"ROOT\"," + + "\"thread\":\"main\",\"message\":\"test good {}\"," + + "\"params\":{\"good\":\":-)\"},\"unsafeParams\":{},\"tags\":{}}"; + private static final String SERVICE_FORMATTED = "ERROR [2019-05-09T15:32:37.692Z] ROOT: test good {} (good: :-))"; + + private static final String REQUEST_JSON = "{\"type\":\"request.2\",\"time\":\"2019-05-24T12:40:36.703-04:00\"," + + "\"method\":\"GET\",\"protocol\":\"HTTP/1.1\",\"path\":\"/api/sleep/{millis}\"," + + "\"params\":{\"host\":\"localhost:8443\",\"connection\":\"Keep-Alive\"," + + "\"accept-encoding\":\"gzip\",\"user-agent\":\"okhttp/3.13.1\"}," + + "\"status\":503,\"requestSize\":0,\"responseSize\":78,\"duration\":1935," + + "\"traceId\":\"ba3200b6eb01999a\",\"unsafeParams\":{\"path\":\"/api/sleep/10\"," + + "\"millis\":\"10\"}}"; + private static final String REQUEST_FORMATTED = + "[2019-05-24T16:40:36.703Z] \"GET /api/sleep/10 HTTP/1.1\" " + "503 78 1935"; + + private static final String METRIC_JSON = "{\"type\": \"metric.1\"," + + "\"time\":\"2019-05-24T16:40:52.162Z\"," + + "\"metricName\":\"jvm.heap\",\"metricType\":\"gauge\",\"values\":{\"size\":66274352}," + + "\"tags\":{\"collection\":\"Metaspace\",\"collector\":\"PS Scavenge\"," + + "\"when\":\"after\"},\"unsafeParams\":{}}"; + private static final String METRIC_FORMATTED = "[2019-05-24T16:40:52.162Z] METRIC jvm.heap gauge (size: 66274352) " + + "(collection: Metaspace, collector: PS Scavenge, when: after)"; + + private static final String TRACE_JSON = "{\"type\":\"trace.1\",\"time\":\"2019-05-24T16:40:40.95Z\"," + + "\"unsafeParams\":{},\"span\":{\"traceId\":\"2250486695021e19\",\"id\":\"c11b9a31555b7035\"," + + "\"name\":\"config-reload\",\"timestamp\":1558716040949000,\"duration\":618," + + "\"annotations\":[{\"timestamp\":1558716040949000,\"value\":\"lc\"," + + "\"endpoint\":{\"serviceName\":\"my-service\",\"ipv4\":\"10.193.122.103\"}}]}}"; + private static final String TRACE_FORMATTED = "[2019-05-24T16:40:40.950Z] traceId: 2250486695021e19 " + + "id: c11b9a31555b7035 name: config-reload duration: 618 microseconds"; + + private static final String NEWLINE = "\n"; + + private WitchcraftLogSettings settings = WitchcraftLogSettings.builder() + .showEventLogs(true) + .showMetricLogs(true) + .showRequestLogs(true) + .showTraceLogs(true) + .build(); + private final InputFilter filter = new WitchcraftLogFilter(WitchcraftLogFormatter.INSTANCE, () -> settings); + + @Test + public void passesThroughNonWitchcraftLines() { + assertThat(runFilter("foo")) + .describedAs("unmatched lines should result in null, meaning no modification") + .isNull(); + } + + @Test + public void formatEvent() { + assertThat(runFilter(EVENT_JSON)).isEqualTo(EVENT_FORMATTED); + } + + @Test + public void testEventContentType() { + assertThat(runFilterWithType(EVENT_JSON)) + .extracting(pair -> pair.second) + .containsOnly(WitchcraftConsoleViewContentTypes.EVENT_TYPE); + } + + @Test + public void formatServiceLine() { + assertThat(runFilter(SERVICE_JSON)).isEqualTo(SERVICE_FORMATTED); + } + + @Test + public void formatServiceLine_slf4jInterpolation() { + assertThat(runFilter("{\"type\":\"service.1\",\"level\":\"ERROR\"," + + "\"time\":\"2019-05-09T15:32:37.692Z\",\"origin\":\"ROOT\"," + + "\"thread\":\"main\",\"message\":\"Hello, {}!\"," + + "\"params\":{},\"unsafeParams\":{\"0\": \"World\"},\"tags\":{}}")) + .isEqualTo("ERROR [2019-05-09T15:32:37.692Z] ROOT: Hello, World! (0: World)"); + } + + @Test + public void formatRequestLine() { + assertThat(runFilter(REQUEST_JSON)).isEqualTo(REQUEST_FORMATTED); + } + + @Test + public void requestLogContentType() { + assertThat(runFilterWithType(REQUEST_JSON)) + .extracting(pair -> pair.second) + .containsOnly(WitchcraftConsoleViewContentTypes.REQUEST_TYPE); + } + + @Test + public void formatMetricLine() { + assertThat(runFilter(METRIC_JSON)).isEqualTo(METRIC_FORMATTED); + } + + @Test + public void formatTrace() { + assertThat(runFilter(TRACE_JSON)).isEqualTo(TRACE_FORMATTED); + } + + @Test + public void suppressTraceWhenDisabled() { + settings = WitchcraftLogSettings.builder() + .from(settings) + .showTraceLogs(false) + .build(); + + assertThat(runFilter(TRACE_JSON + NEWLINE)) + .describedAs("Returns empty to suppress normal output") + .isEmpty(); + } + + @Test + public void suppressMultiline() { + settings = WitchcraftLogSettings.builder() + .from(settings) + .showTraceLogs(false) + .build(); + + assertThat(runFilter(EVENT_JSON + NEWLINE + TRACE_JSON + NEWLINE + NEWLINE + SERVICE_JSON)) + .describedAs("Returns only unsuppressed logs, removes exactly one newline") + .isEqualTo(EVENT_FORMATTED + NEWLINE + NEWLINE + SERVICE_FORMATTED); + } + + @Test + public void fastPredicateTest_nonWitchcraft() { + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock("foobar")).isFalse(); + } + + @Test + public void fastPredicateTest_newlineBrokenWitchcraftLog() { + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock("{\n" + METRIC_JSON.substring(1))) + .describedAs("Newlines are illegal in Witchcraft logs, don't attempt to match") + .isFalse(); + } + + @Test + public void fastPredicateTest_metric() { + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock(METRIC_JSON)).isTrue(); + } + + @Test + public void fastPredicateTest_metricNewUnsupported() { + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock("{\"type\":\"metric.5\"," + + "\"time\":\"2019-05-24T16:40:52.162Z\"," + + "\"metricName\":\"jvm.heap\",\"metricType\":\"gauge\",\"values\":{\"size\":66274352}," + + "\"tags\":{\"collection\":\"Metaspace\",\"collector\":\"PS Scavenge\"," + + "\"when\":\"after\"},\"unsafeParams\":{}}")) + .isFalse(); + } + + @Test + public void fastPredicateTest_typeWithSpaces() { + // space after '"type": ', slightly different from the default compact formatting + assertThat(LogParser.anyPossibleWitchcraftLogsInBlock("{\"type\": \"metric.1\"," + + "\"time\":\"2019-05-24T16:40:52.162Z\"," + + "\"metricName\":\"jvm.heap\",\"metricType\":\"gauge\",\"values\":{\"size\":66274352}," + + "\"tags\":{\"collection\":\"Metaspace\",\"collector\":\"PS Scavenge\"," + + "\"when\":\"after\"},\"unsafeParams\":{}}")) + .isTrue(); + } + + @Test + public void testMultiline() { + assertThat(runFilter(METRIC_JSON + NEWLINE + METRIC_JSON)) + .isEqualTo(METRIC_FORMATTED + NEWLINE + METRIC_FORMATTED); + } + + @Nullable + private String runFilter(String input) { + List> pairs = runFilterWithType(input); + if (pairs == null) { + return null; + } + + return pairs.stream().map(pair -> pair.first).collect(Collectors.joining()); + } + + @Nullable + private List> runFilterWithType(String input) { + return filter.applyFilter(input, ConsoleViewContentType.NORMAL_OUTPUT); + } +} diff --git a/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsSerializationTest.java b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsSerializationTest.java new file mode 100644 index 00000000..24fb7c5d --- /dev/null +++ b/witchcraft-logging-idea/src/test/java/com/palantir/witchcraft/logging/idea/WitchcraftLogSettingsSerializationTest.java @@ -0,0 +1,82 @@ +/* + * (c) Copyright 2021 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.witchcraft.logging.idea; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.intellij.util.xmlb.XmlSerializer; +import java.util.Map; +import java.util.stream.Collectors; +import org.jdom.Attribute; +import org.jdom.Element; +import org.junit.jupiter.api.Test; + +public class WitchcraftLogSettingsSerializationTest { + @Test + public void testSerialize() { + Element element = XmlSerializer.serialize(ModifiableWitchcraftLogSettings.create()); + assertThat(element.getName()).isEqualTo("WitchcraftLogSettings"); + + assertThat(element.getChildren()).allSatisfy(child -> { + assertThat(child.getName()).isEqualTo("option"); + assertThat(child.getChildren()).isEmpty(); + assertThat(child.getAttributes()).extracting(Attribute::getName).containsExactlyInAnyOrder("name", "value"); + }); + + assertThat(getOptions(element)) + .hasSize(4) + .containsEntry("showEventLogs", "true") + .containsEntry("showMetricLogs", "false") + .containsEntry("showRequestLogs", "true") + .containsEntry("showTraceLogs", "true"); + } + + @Test + public void testDeserializeEmptyUsesDefaults() { + ModifiableWitchcraftLogSettings defaults = ModifiableWitchcraftLogSettings.create(); + + Element element = new Element("WitchcraftLogSettings"); + ModifiableWitchcraftLogSettings settings = + XmlSerializer.deserialize(element, ModifiableWitchcraftLogSettings.class); + assertThat(settings).isEqualTo(defaults); + } + + @Test + public void testDeserializeOverridesDefaults() { + ModifiableWitchcraftLogSettings nonDefaults = ModifiableWitchcraftLogSettings.create(); + nonDefaults.setShowEventLogs(!nonDefaults.getShowEventLogs()); + + Element element = new Element("WitchcraftLogSettings"); + setOption(element, "showEventLogs", Boolean.toString(nonDefaults.getShowEventLogs())); + ModifiableWitchcraftLogSettings settings = + XmlSerializer.deserialize(element, ModifiableWitchcraftLogSettings.class); + assertThat(settings).isEqualTo(nonDefaults); + } + + private static Map getOptions(Element element) { + return element.getChildren("option").stream() + .collect(Collectors.toMap( + option -> option.getAttributeValue("name"), option -> option.getAttributeValue("value"))); + } + + private static void setOption(Element element, String name, String value) { + Element option = new Element("option"); + option.setAttribute("name", name); + option.setAttribute("value", value); + element.addContent(option); + } +}