| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.solr.handler.admin; |
| |
| import org.apache.commons.io.IOUtils; |
| import org.apache.solr.handler.RequestHandlerBase; |
| import org.apache.solr.request.SolrQueryRequest; |
| import org.apache.solr.client.solrj.impl.XMLResponseParser; |
| import org.apache.solr.core.SolrInfoMBean; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.SolrException.ErrorCode; |
| import org.apache.solr.common.util.ContentStream; |
| import org.apache.solr.common.util.NamedList; |
| import org.apache.solr.common.util.SimpleOrderedMap; |
| import org.apache.solr.response.BinaryResponseWriter; |
| import org.apache.solr.response.SolrQueryResponse; |
| |
| import java.io.StringReader; |
| import java.net.URL; |
| import java.text.NumberFormat; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.Map; |
| import java.util.HashSet; |
| |
| /** |
| * A request handler that provides info about all |
| * registered SolrInfoMBeans. |
| */ |
| @SuppressWarnings("unchecked") |
| public class SolrInfoMBeanHandler extends RequestHandlerBase { |
| |
| // back-compat names, only needed in 6.x - see SOLR-10035 |
| static final Map<String, String> newToOldCategories = new HashMap<String, String>() {{ |
| put(Category.QUERY.toString(), "QUERYHANDLER"); |
| put(Category.UPDATE.toString(), "UPDATEHANDLER"); |
| put(Category.HIGHLIGHTER.toString(), "HIGHLIGHTING"); |
| }}; |
| static final Map<String, String> oldToNewCategories = new HashMap<String, String>() {{ |
| put("QUERYHANDLER", Category.QUERY.toString()); |
| put("UPDATEHANDLER", Category.UPDATE.toString()); |
| put("HIGHLIGHTING", Category.HIGHLIGHTER.toString()); |
| }}; |
| |
| static final boolean useOnlyNewNaming = Boolean.valueOf(System.getProperty("solr.mbeans.useOnlyNewNaming", "false")); |
| |
| |
| /** |
| * Take an array of any type and generate a Set containing the toString. |
| * Set is guarantee to never be null (but may be empty) |
| */ |
| private Set<String> arrayToSet(Object[] arr) { |
| HashSet<String> r = new HashSet<>(); |
| if (null == arr) return r; |
| for (Object o : arr) { |
| if (null != o) r.add(o.toString()); |
| } |
| return r; |
| } |
| |
| @Override |
| public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { |
| NamedList<NamedList<NamedList<Object>>> cats = getMBeanInfo(req); |
| if(req.getParams().getBool("diff", false)) { |
| ContentStream body = null; |
| try { |
| body = req.getContentStreams().iterator().next(); |
| } |
| catch(Exception ex) { |
| throw new SolrException(ErrorCode.BAD_REQUEST, "missing content-stream for diff"); |
| } |
| String content = IOUtils.toString(body.getReader()); |
| |
| NamedList<NamedList<NamedList<Object>>> ref = fromXML(content); |
| |
| |
| // Normalize the output |
| SolrQueryResponse wrap = new SolrQueryResponse(); |
| wrap.add("solr-mbeans", cats); |
| cats = (NamedList<NamedList<NamedList<Object>>>) |
| BinaryResponseWriter.getParsedResponse(req, wrap).get("solr-mbeans"); |
| |
| // Get rid of irrelevant things |
| ref = normalize(ref); |
| cats = normalize(cats); |
| |
| // Only the changes |
| boolean showAll = req.getParams().getBool("all", false); |
| rsp.add("solr-mbeans", getDiff(ref,cats, showAll)); |
| } |
| else { |
| rsp.add("solr-mbeans", cats); |
| } |
| rsp.setHttpCaching(false); // never cache, no matter what init config looks like |
| } |
| |
| static NamedList<NamedList<NamedList<Object>>> fromXML(String content) { |
| int idx = content.indexOf("<response>"); |
| if(idx<0) { |
| throw new SolrException(ErrorCode.BAD_REQUEST, "Body does not appear to be an XML response"); |
| } |
| |
| try { |
| XMLResponseParser parser = new XMLResponseParser(); |
| return (NamedList<NamedList<NamedList<Object>>>) |
| parser.processResponse(new StringReader(content)).get("solr-mbeans"); |
| } |
| catch(Exception ex) { |
| throw new SolrException(ErrorCode.BAD_REQUEST, "Unable to read original XML", ex); |
| } |
| } |
| |
| protected NamedList<NamedList<NamedList<Object>>> getMBeanInfo(SolrQueryRequest req) { |
| |
| NamedList<NamedList<NamedList<Object>>> cats = new NamedList<>(); |
| |
| String[] requestedCats = req.getParams().getParams("cat"); |
| if (null == requestedCats || 0 == requestedCats.length) { |
| for (SolrInfoMBean.Category cat : SolrInfoMBean.Category.values()) { |
| cats.add(cat.name(), new SimpleOrderedMap<NamedList<Object>>()); |
| } |
| if (!useOnlyNewNaming) { |
| for (String oldName : newToOldCategories.values()) { |
| cats.add(oldName, new SimpleOrderedMap<NamedList<Object>>()); |
| } |
| } |
| } else { |
| for (String catName : requestedCats) { |
| cats.add(catName,new SimpleOrderedMap<NamedList<Object>>()); |
| if (!useOnlyNewNaming) { |
| if (newToOldCategories.containsKey(catName)) { |
| cats.add(newToOldCategories.get(catName), new SimpleOrderedMap<NamedList<Object>>()); |
| } |
| if (oldToNewCategories.containsKey(catName)) { |
| cats.add(oldToNewCategories.get(catName), new SimpleOrderedMap<NamedList<Object>>()); |
| } |
| } |
| } |
| } |
| |
| Set<String> requestedKeys = arrayToSet(req.getParams().getParams("key")); |
| |
| Map<String, SolrInfoMBean> reg = req.getCore().getInfoRegistry(); |
| for (Map.Entry<String, SolrInfoMBean> entry : reg.entrySet()) { |
| String cat = entry.getValue().getCategory().name(); |
| addMBean(req, cat, cats, requestedKeys, entry.getKey(),entry.getValue()); |
| // add it also under back-compat name |
| if (!useOnlyNewNaming && newToOldCategories.containsKey(cat)) { |
| addMBean(req, newToOldCategories.get(cat), cats, requestedKeys, entry.getKey(),entry.getValue()); |
| } |
| } |
| |
| for (SolrInfoMBean infoMBean : req.getCore().getCoreDescriptor().getCoreContainer().getResourceLoader().getInfoMBeans()) { |
| String cat = infoMBean.getCategory().name(); |
| addMBean(req,cat, cats,requestedKeys,infoMBean.getName(),infoMBean); |
| // add it also under back-compat name |
| if (!useOnlyNewNaming && newToOldCategories.containsKey(cat)) { |
| addMBean(req, newToOldCategories.get(cat), cats, requestedKeys, infoMBean.getName(), infoMBean); |
| } |
| } |
| return cats; |
| } |
| |
| private void addMBean(SolrQueryRequest req, String categoryName, NamedList<NamedList<NamedList<Object>>> cats, Set<String> requestedKeys, String key, SolrInfoMBean m) { |
| if ( ! ( requestedKeys.isEmpty() || requestedKeys.contains(key) ) ) return; |
| NamedList<NamedList<Object>> catInfo = cats.get(categoryName); |
| if ( null == catInfo ) return; |
| NamedList<Object> mBeanInfo = new SimpleOrderedMap<>(); |
| mBeanInfo.add("class", m.getName()); |
| mBeanInfo.add("version", m.getVersion()); |
| mBeanInfo.add("description", m.getDescription()); |
| mBeanInfo.add("src", m.getSource()); |
| |
| // Use an external form |
| URL[] urls = m.getDocs(); |
| if(urls!=null) { |
| List<String> docs = new ArrayList<>(urls.length); |
| for(URL url : urls) { |
| docs.add(url.toExternalForm()); |
| } |
| mBeanInfo.add("docs", docs); |
| } |
| |
| if (req.getParams().getFieldBool(key, "stats", false)) |
| mBeanInfo.add("stats", m.getStatistics()); |
| |
| catInfo.add(key, mBeanInfo); |
| } |
| |
| protected NamedList<NamedList<NamedList<Object>>> getDiff( |
| NamedList<NamedList<NamedList<Object>>> ref, |
| NamedList<NamedList<NamedList<Object>>> now, |
| boolean includeAll ) { |
| |
| NamedList<NamedList<NamedList<Object>>> changed = new NamedList<>(); |
| |
| // Cycle through each category |
| for(int i=0;i<ref.size();i++) { |
| String category = ref.getName(i); |
| NamedList<NamedList<Object>> ref_cat = ref.get(category); |
| NamedList<NamedList<Object>> now_cat = now.get(category); |
| if(now_cat != null) { |
| String ref_txt = ref_cat+""; |
| String now_txt = now_cat+""; |
| if(!ref_txt.equals(now_txt)) { |
| // Something in the category changed |
| // Now iterate the real beans |
| |
| NamedList<NamedList<Object>> cat = new SimpleOrderedMap<>(); |
| for(int j=0;j<ref_cat.size();j++) { |
| String name = ref_cat.getName(j); |
| NamedList<Object> ref_bean = ref_cat.get(name); |
| NamedList<Object> now_bean = now_cat.get(name); |
| |
| ref_txt = ref_bean+""; |
| now_txt = now_bean+""; |
| if(!ref_txt.equals(now_txt)) { |
| // System.out.println( "----" ); |
| // System.out.println( category +" : " + name ); |
| // System.out.println( "REF: " + ref_txt ); |
| // System.out.println( "NOW: " + now_txt ); |
| |
| // Calculate the differences |
| NamedList diff = diffNamedList(ref_bean,now_bean); |
| diff.add( "_changed_", true ); // flag the changed thing |
| cat.add(name, diff); |
| } |
| else if(includeAll) { |
| cat.add(name, ref_bean); |
| } |
| } |
| if(cat.size()>0) { |
| changed.add(category, cat); |
| } |
| } |
| else if(includeAll) { |
| changed.add(category, ref_cat); |
| } |
| } |
| } |
| return changed; |
| } |
| |
| public NamedList diffNamedList(NamedList ref, NamedList now) { |
| NamedList out = new SimpleOrderedMap(); |
| for(int i=0; i<ref.size(); i++) { |
| String name = ref.getName(i); |
| Object r = ref.getVal(i); |
| Object n = now.remove(name); |
| if(n == null) { |
| if(r!=null) { |
| out.add("REMOVE "+name, r); |
| } |
| } |
| else { |
| out.add(name, diffObject(r,n)); |
| } |
| } |
| |
| for(int i=0; i<now.size(); i++) { |
| String name = now.getName(i); |
| Object v = now.getVal(i); |
| if(v!=null) { |
| out.add("ADD "+name, v); |
| } |
| } |
| return out; |
| } |
| |
| public Object diffObject(Object ref, Object now) { |
| if(ref instanceof NamedList) { |
| return diffNamedList((NamedList)ref, (NamedList)now); |
| } |
| if(ref.equals(now)) { |
| return ref; |
| } |
| StringBuilder str = new StringBuilder(); |
| str.append("Was: ") |
| .append(ref).append(", Now: ").append(now); |
| |
| if(ref instanceof Number) { |
| NumberFormat nf = NumberFormat.getIntegerInstance(Locale.ROOT); |
| if((ref instanceof Double) || (ref instanceof Float)) { |
| nf = NumberFormat.getInstance(Locale.ROOT); |
| } |
| double dref = ((Number)ref).doubleValue(); |
| double dnow = ((Number)now).doubleValue(); |
| double diff = Double.NaN; |
| if(Double.isNaN(dref)) { |
| diff = dnow; |
| } |
| else if(Double.isNaN(dnow)) { |
| diff = dref; |
| } |
| else { |
| diff = dnow-dref; |
| } |
| str.append( ", Delta: ").append(nf.format(diff)); |
| } |
| return str.toString(); |
| } |
| |
| |
| /** |
| * The 'avgRequestsPerSecond' field will make everything look like it changed |
| */ |
| public NamedList normalize(NamedList input) { |
| input.remove("avgRequestsPerSecond"); |
| for(int i=0; i<input.size(); i++) { |
| Object v = input.getVal(i); |
| if(v instanceof NamedList) { |
| input.setVal(i, normalize((NamedList)v)); |
| } |
| } |
| return input; |
| } |
| |
| |
| @Override |
| public String getDescription() { |
| return "Get Info (and statistics) for registered SolrInfoMBeans"; |
| } |
| |
| @Override |
| public Category getCategory() { |
| return Category.ADMIN; |
| } |
| } |