Skip to content

Commit

Permalink
Auto-detect service name from web.xml display-name (#886)
Browse files Browse the repository at this point in the history
* Auto-detect service name from web.xml display-name

* lazy init sax parser factory

* add comment

* add liberty

* add glassfish and wildfly, test auto detected name in smoke tests

* fixes

* add websphere

* debug wildfly on windows

* wildfly on windows

* degugging

* fix JBOSS_BASE_DIR handling

* modify tests, fix tomee

* Apply suggestions from code review

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>

* add comments

* fix liberty custom WLP_OUTPUT_DIR handling

* spotless

* address review comments

* update test

* add jetty.base handling

* spotless

* close directory stream

* convert to resource provider

* remame method

* Update custom/src/main/java/com/splunk/opentelemetry/servicename/ServiceNameResourceProvider.java

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>
  • Loading branch information
laurit and Mateusz Rzeszutek committed Sep 2, 2022
1 parent 1bb18de commit 653586e
Show file tree
Hide file tree
Showing 37 changed files with 1,344 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
/*
* Copyright Splunk Inc.
*
* 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.splunk.opentelemetry.servicename;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.annotation.Nullable;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

abstract class AppServerServiceNameDetector extends ServiceNameDetector {
private static final Logger log = LoggerFactory.getLogger(AppServerServiceNameDetector.class);

final ResourceLocator locator;
final Class<?> serverClass;
final boolean supportsEar;

AppServerServiceNameDetector(
ResourceLocator locator, String serverClassName, boolean supportsEar) {
this.locator = locator;
this.serverClass = locator.findClass(serverClassName);
this.supportsEar = supportsEar;
}

/** Use to ignore default applications that are bundled with the app server. */
boolean isValidAppName(Path path) {
return true;
}

/** Use to ignore default applications that are bundled with the app server. */
boolean isValidResult(Path path, @Nullable String result) {
return true;
}

/** Path to directory to be scanned for deployments. */
abstract Path getDeploymentDir() throws Exception;

@Override
String detect() throws Exception {
if (serverClass == null) {
return null;
}

Path deploymentDir = getDeploymentDir();
if (deploymentDir == null) {
return null;
}

if (Files.isDirectory(deploymentDir)) {
log.debug("Looking for deployments in '{}'.", deploymentDir);
try (Stream<Path> stream = Files.list(deploymentDir)) {
for (Path path : stream.collect(Collectors.toList())) {
String name = detectName(path);
if (name != null) {
return name;
}
}
}
} else {
log.debug("Deployment dir '{}' doesn't exist.", deploymentDir);
}

return null;
}

private String detectName(Path path) {
if (!isValidAppName(path)) {
log.debug("Skipping '{}'.", path);
return null;
}

log.debug("Attempting service name detection in '{}'.", path);
String name = path.getFileName().toString();
if (Files.isDirectory(path)) {
return handleExplodedApp(path);
} else if (name.endsWith(".war")) {
return handlePackagedWar(path);
} else if (supportsEar && name.endsWith(".ear")) {
return handlePackagedEar(path);
}

return null;
}

private String handleExplodedApp(Path path) {
{
String result = handleExplodedWar(path);
if (result != null) {
return result;
}
}
if (supportsEar) {
String result = handleExplodedEar(path);
if (result != null) {
return result;
}
}
return null;
}

private String handlePackagedWar(Path path) {
return handlePackaged(path, "WEB-INF/web.xml", new WebXmlHandler());
}

private String handlePackagedEar(Path path) {
return handlePackaged(path, "META-INF/application.xml", new ApplicationXmlHandler());
}

private String handlePackaged(Path path, String descriptorPath, DescriptorHandler handler) {
try (ZipFile zip = new ZipFile(path.toFile())) {
ZipEntry zipEntry = zip.getEntry(descriptorPath);
if (zipEntry != null) {
return handle(() -> zip.getInputStream(zipEntry), path, handler);
}
} catch (IOException exception) {
log.warn("Failed to read '{}' from zip '{}'.", descriptorPath, path, exception);
}

return null;
}

String handleExplodedWar(Path path) {
return handleExploded(path, path.resolve("WEB-INF/web.xml"), new WebXmlHandler());
}

String handleExplodedEar(Path path) {
return handleExploded(
path, path.resolve("META-INF/application.xml"), new ApplicationXmlHandler());
}

private String handleExploded(Path path, Path descriptor, DescriptorHandler handler) {
if (Files.isRegularFile(descriptor)) {
return handle(() -> Files.newInputStream(descriptor), path, handler);
}

return null;
}

private String handle(InputStreamSupplier supplier, Path path, DescriptorHandler handler) {
try {
try (InputStream inputStream = supplier.supply()) {
String candidate = parseDescriptor(inputStream, handler);
if (isValidResult(path, candidate)) {
return candidate;
}
}
} catch (Exception exception) {
log.warn("Failed to parse descriptor", exception);
}

return null;
}

private static String parseDescriptor(InputStream inputStream, DescriptorHandler handler)
throws ParserConfigurationException, SAXException, IOException {
if (SaxParserFactoryHolder.saxParserFactory == null) {
return null;
}
SAXParser saxParser = SaxParserFactoryHolder.saxParserFactory.newSAXParser();
saxParser.parse(inputStream, handler);
return handler.displayName;
}

private interface InputStreamSupplier {
InputStream supply() throws IOException;
}

private static class WebXmlHandler extends DescriptorHandler {

WebXmlHandler() {
super("web-app");
}
}

private static class ApplicationXmlHandler extends DescriptorHandler {

ApplicationXmlHandler() {
super("application");
}
}

private static class DescriptorHandler extends DefaultHandler {
private final String rootElementName;
private final Deque<String> currentElement = new ArrayDeque<>();
private boolean setDisplayName;
String displayName;

DescriptorHandler(String rootElementName) {
this.rootElementName = rootElementName;
}

@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
if (displayName == null
&& rootElementName.equals(currentElement.peek())
&& "display-name".equals(qName)) {
String lang = attributes.getValue("xml:lang");
if (lang == null || "".equals(lang)) {
lang = "en"; // en is the default language
}
if ("en".equals(lang)) {
setDisplayName = true;
}
}
currentElement.push(qName);
}

@Override
public void endElement(String uri, String localName, String qName) {
currentElement.pop();
setDisplayName = false;
}

@Override
public void characters(char[] ch, int start, int length) {
if (setDisplayName) {
displayName = new String(ch, start, length);
}
}
}

private static class SaxParserFactoryHolder {
private static final SAXParserFactory saxParserFactory = getSaxParserFactory();

private static SAXParserFactory getSaxParserFactory() {
try {
return SAXParserFactory.newInstance();
} catch (Throwable throwable) {
log.debug("XML parser not available.", throwable);
}
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Splunk Inc.
*
* 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.splunk.opentelemetry.servicename;

import java.nio.file.Path;
import java.nio.file.Paths;

class GlassfishServiceNameDetector extends AppServerServiceNameDetector {

GlassfishServiceNameDetector(ResourceLocator locator) {
super(locator, "com.sun.enterprise.glassfish.bootstrap.ASMain", true);
}

@Override
Path getDeploymentDir() {
String instanceRoot = System.getProperty("com.sun.aas.instanceRoot");
if (instanceRoot == null) {
return null;
}

// besides autodeploy directory it is possible to deploy applications through admin console and
// asadmin script, to detect those we would need to parse config/domain.xml
return Paths.get(instanceRoot, "autodeploy");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright Splunk Inc.
*
* 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.splunk.opentelemetry.servicename;

import com.google.common.annotations.VisibleForTesting;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class JettyServiceNameDetector extends AppServerServiceNameDetector {
private static final Logger log = LoggerFactory.getLogger(JettyServiceNameDetector.class);

JettyServiceNameDetector(ResourceLocator locator) {
super(locator, "org.eclipse.jetty.start.Main", false);
}

@Override
boolean isValidAppName(Path path) {
// jetty deployer ignores directories ending with ".d"
if (Files.isDirectory(path)) {
return !path.getFileName().toString().endsWith(".d");
}
return true;
}

@VisibleForTesting
static Path parseJettyBase(String programArguments) {
if (programArguments == null) {
return null;
}
int start = programArguments.indexOf("jetty.base=");
if (start == -1) {
return null;
}
start += "jetty.base=".length();
if (start == programArguments.length()) {
return null;
}
// Take the path until the first space. If the path doesn't exist extend it up to the next
// space. Repeat until a path that exists is found or input runs out.
int next = start;
while (true) {
int nextSpace = programArguments.indexOf(' ', next);
if (nextSpace == -1) {
Path candidate = Paths.get(programArguments.substring(start));
return Files.exists(candidate) ? candidate : null;
}
Path candidate = Paths.get(programArguments.substring(start, nextSpace));
next = nextSpace + 1;
if (Files.exists(candidate)) {
return candidate;
}
}
}

@Override
Path getDeploymentDir() {
// Jetty expects the webapps directory to be in the directory where jetty was started from.
// Alternatively the location of webapps directory can be specified by providing jetty base
// directory as an argument to jetty e.g. java -jar start.jar jetty.base=/dir where webapps
// would be located in /dir/webapps.

String programArguments = System.getProperty("sun.java.command");
log.debug("Started with arguments '{}'.", programArguments);
if (programArguments != null) {
Path jettyBase = parseJettyBase(programArguments);
if (jettyBase != null) {
log.debug("Using jetty.base '{}'.", jettyBase);
return jettyBase.resolve("webapps");
}
}

return Paths.get("webapps").toAbsolutePath();
}
}

0 comments on commit 653586e

Please sign in to comment.