Add bundles-diag
[odlparent.git] / karaf / bundles-diag / src / main / java / org / opendaylight / odlparent / bundles / diag / spi / DefaultDiag.java
diff --git a/karaf/bundles-diag/src/main/java/org/opendaylight/odlparent/bundles/diag/spi/DefaultDiag.java b/karaf/bundles-diag/src/main/java/org/opendaylight/odlparent/bundles/diag/spi/DefaultDiag.java
new file mode 100644 (file)
index 0000000..2f743a2
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.odlparent.bundles.diag.spi;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.odlparent.bundles.diag.ContainerState;
+import org.opendaylight.odlparent.bundles.diag.Diag;
+import org.opendaylight.odlparent.bundles.diag.DiagBundle;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.slf4j.Logger;
+
+/**
+ * The default {@link Diag} implementation.
+ */
+record DefaultDiag(BundleContext bundleContext, List<DiagBundle> bundles) implements Diag {
+    private static final Map<String, ContainerState> ALLOWED_STATES = Map.of(
+        "slf4j.log4j12", ContainerState.INSTALLED,
+        // ODLPARENT-144
+        "org.apache.karaf.scr.management", ContainerState.WAITING);
+
+    DefaultDiag {
+        requireNonNull(bundleContext);
+        requireNonNull(bundles);
+    }
+
+    @Override
+    public void logDelta(final Logger logger, final Diag prevDiag) {
+        final var prevBundles = new ArrayDeque<>(prevDiag.bundles());
+        // Log current state ...
+        for (var bundle : bundles) {
+            if (!bundle.equals(find(logger, prevBundles, bundle.bundleId()))) {
+                logger.info("Updated {}:{} {}/{}", bundle.symbolicName(), bundle.version(), bundle.frameworkState(),
+                    serviceStateString(bundle));
+            }
+        }
+
+        // everything else is not present
+        prevBundles.forEach(bundle -> logger.info("{} no longer present", bundle));
+    }
+
+    static @Nullable DiagBundle find(final Logger logger, final ArrayDeque<DiagBundle> bundles, final long bundleId) {
+        for (var bundle = bundles.poll(); bundle != null; bundle = bundles.poll()) {
+            final var id = bundle.bundleId();
+            if (id == bundleId) {
+                return bundle;
+            } else if (id > bundleId) {
+                bundles.addFirst(bundle);
+                break;
+            }
+            logger.info("{} no longer present", bundle);
+        }
+        return null;
+    }
+
+    @Override
+    public void logServices(final Logger logger) {
+        logger.info("""
+            Now going to log all known services, to help diagnose root cause of diag failure BundleService reported \
+            bundle(s) which are not active""");
+        try {
+            for (var serviceRef : bundleContext.getAllServiceReferences(null, null)) {
+                var bundle = serviceRef.getBundle();
+                // serviceRef.getBundle() can return null if the bundle was destroyed
+                if (bundle != null) {
+                    if (logger.isInfoEnabled()) {
+                        logger.info("{} defines OSGi Service {} used by {}", bundle.getSymbolicName(),
+                            getProperties(serviceRef), getUsingBundleSymbolicNames(serviceRef));
+                    }
+                } else {
+                    logger.trace("skipping reporting service reference as the underlying bundle is null");
+                }
+            }
+        } catch (InvalidSyntaxException e) {
+            logger.error("Failed due to InvalidSyntaxException", e);
+        }
+    }
+
+    // Visible for testing
+    static Map<String, Object> getProperties(final ServiceReference<?> serviceRef) {
+        final var propertyKeys = serviceRef.getPropertyKeys();
+        final var properties = new HashMap<String, Object>(propertyKeys.length);
+        for (var propertyKey : propertyKeys) {
+            var propertyValue = serviceRef.getProperty(propertyKey);
+            if (propertyValue != null) {
+                if (propertyValue.getClass().isArray()) {
+                    propertyValue = Arrays.asList((Object[]) propertyValue);
+                }
+            }
+            // maintain the null value in the property map anyway
+            properties.put(propertyKey, propertyValue);
+        }
+        return properties;
+    }
+
+    // Visible for testing
+    static List<String> getUsingBundleSymbolicNames(final ServiceReference<?> serviceRef) {
+        final var usingBundles = serviceRef.getUsingBundles();
+        return usingBundles == null ? List.of()
+            : Arrays.stream(usingBundles).map(Bundle::getSymbolicName).collect(Collectors.toList());
+    }
+
+    @Override
+    public void logState(final Logger logger) {
+        try {
+            logServices(logger);
+        } catch (IllegalStateException e) {
+            logger.warn("logOSGiServices() failed (never mind); too late during shutdown already?", e);
+        }
+
+        final var okBundles = new ArrayList<DiagBundle>();
+        final var allowedBundles = new ArrayList<DiagBundle>();
+        final var nokBundles = new ArrayList<DiagBundle>();
+
+        for (var bundle : bundles) {
+            final var serviceState = bundle.serviceState();
+            // BundleState comparison as in Karaf's "diag" command, see
+            // https://github.com/apache/karaf/blob/master/bundle/core/src/main/java/org/apache/karaf/bundle/command/Diag.java
+            // but we intentionally, got a little further than Karaf's "diag" command, and instead of only checking some
+            // states, we check what's really Active, but accept that some remain just Resolved:
+            final var containerState = serviceState.containerState();
+
+            final var list = switch (containerState) {
+                case ACTIVE, RESOLVED -> okBundles;
+                default -> {
+                    final var symbolicName = bundle.symbolicName();
+                    yield symbolicName != null && containerState.equals(ALLOWED_STATES.get(symbolicName))
+                        ? allowedBundles : nokBundles;
+                }
+            };
+            list.add(bundle);
+        }
+
+        if (logger.isInfoEnabled()) {
+            for (var bundle : okBundles) {
+                logger.info("OK {}:{} {}/{}", bundle.symbolicName(), bundle.version(), bundle.frameworkState(),
+                    serviceStateString(bundle));
+            }
+        }
+        if (logger.isWarnEnabled()) {
+            for (var bundle : allowedBundles) {
+                logger.warn("WHITELISTED {}:{} {}/{}", bundle.symbolicName(), bundle.version(), bundle.frameworkState(),
+                    serviceStateString(bundle));
+            }
+        }
+        if (logger.isErrorEnabled()) {
+            for (var bundle : nokBundles) {
+                logger.error("NOK {}:{} {}/{}", bundle.symbolicName(), bundle.version(), bundle.frameworkState(),
+                    serviceStateString(bundle));
+            }
+        }
+    }
+
+    private static String serviceStateString(final DiagBundle bundle) {
+        final var serviceState = bundle.serviceState();
+        final var diag = serviceState.diag();
+        return serviceState.containerState().reportingName() + (diag.isBlank() ? "" : ", due to: " + diag);
+    }
+}