Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-detect service name from web.xml display-name #886

Merged
merged 24 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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();
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
} 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;
}
}
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
}

@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();
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
}
}