All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.brooklyn.feed.windows.WindowsPerformanceCounterFeed Maven / Gradle / Ivy

/*
 * 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.brooklyn.feed.windows;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.mgmt.ExecutionContext;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.effector.EffectorTasks;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.feed.AbstractFeed;
import org.apache.brooklyn.core.feed.PollHandler;
import org.apache.brooklyn.core.feed.Poller;
import org.apache.brooklyn.core.location.Machines;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;

/**
 * A sensor feed that retrieves performance counters from a Windows host and posts the values to sensors.
 *
 * 

To use this feed, you must provide the entity, and a collection of mappings between Windows performance counter * names and Brooklyn attribute sensors.

* *

This feed uses WinRM to invoke the windows utility typeperf to query for a specific set of performance * counters, by name. The values are extracted from the response, and published to the entity's sensors.

* *

Example:

* * {@code * @Override * protected void connectSensors() { * WindowsPerformanceCounterFeed feed = WindowsPerformanceCounterFeed.builder() * .entity(entity) * .addSensor("\\Processor(_total)\\% Idle Time", CPU_IDLE_TIME) * .addSensor("\\Memory\\Available MBytes", AVAILABLE_MEMORY) * .build(); * } * } * * @since 0.6.0 * @author richardcloudsoft */ public class WindowsPerformanceCounterFeed extends AbstractFeed { private static final Logger log = LoggerFactory.getLogger(WindowsPerformanceCounterFeed.class); // This pattern matches CSV line(s) with the date in the first field, and at least one further field. protected static final Pattern lineWithPerfData = Pattern.compile("^\"[\\d:/\\-. ]+\",\".*\"$", Pattern.MULTILINE); private static final Joiner JOINER_ON_SPACE = Joiner.on(' '); private static final Joiner JOINER_ON_COMMA = Joiner.on(','); private static final int OUTPUT_COLUMN_WIDTH = 100; @SuppressWarnings("serial") public static final ConfigKey>> POLLS = ConfigKeys.newConfigKey( new TypeToken>>() {}, "polls"); public static Builder builder() { return new Builder(); } public static class Builder { private Entity entity; private Set> polls = Sets.newLinkedHashSet(); private Duration period = Duration.of(30, TimeUnit.SECONDS); private String uniqueTag; private volatile boolean built; public Builder entity(Entity val) { this.entity = checkNotNull(val, "entity"); return this; } public Builder addSensor(WindowsPerformanceCounterPollConfig config) { polls.add(config); return this; } public Builder addSensor(String performanceCounterName, AttributeSensor sensor) { return addSensor(new WindowsPerformanceCounterPollConfig(sensor).performanceCounterName(checkNotNull(performanceCounterName, "performanceCounterName"))); } public Builder addSensors(Map sensors) { for (Map.Entry entry : sensors.entrySet()) { addSensor(entry.getKey(), entry.getValue()); } return this; } public Builder period(Duration period) { this.period = checkNotNull(period, "period"); return this; } public Builder period(long millis) { return period(millis, TimeUnit.MILLISECONDS); } public Builder period(long val, TimeUnit units) { return period(Duration.of(val, units)); } public Builder uniqueTag(String uniqueTag) { this.uniqueTag = uniqueTag; return this; } public WindowsPerformanceCounterFeed build() { built = true; WindowsPerformanceCounterFeed result = new WindowsPerformanceCounterFeed(this); result.setEntity(checkNotNull((EntityLocal)entity, "entity")); result.start(); return result; } @Override protected void finalize() { if (!built) log.warn("WindowsPerformanceCounterFeed.Builder created, but build() never called"); } } /** * For rebind; do not call directly; use builder */ public WindowsPerformanceCounterFeed() { } protected WindowsPerformanceCounterFeed(Builder builder) { List> polls = Lists.newArrayList(); for (WindowsPerformanceCounterPollConfig config : builder.polls) { if (!config.isEnabled()) continue; @SuppressWarnings({ "unchecked", "rawtypes" }) WindowsPerformanceCounterPollConfig configCopy = new WindowsPerformanceCounterPollConfig(config); if (configCopy.getPeriod() < 0) configCopy.period(builder.period); polls.add(configCopy); } config().set(POLLS, polls); initUniqueTag(builder.uniqueTag, polls); } @Override protected void preStart() { Collection> polls = getConfig(POLLS); long minPeriod = Integer.MAX_VALUE; List performanceCounterNames = Lists.newArrayList(); for (WindowsPerformanceCounterPollConfig config : polls) { minPeriod = Math.min(minPeriod, config.getPeriod()); performanceCounterNames.add(config.getPerformanceCounterName()); } Iterable allParams = ImmutableList.builder() .add("$ProgressPreference = \"SilentlyContinue\";") .add("(Get-Counter") .add("-Counter") .add(JOINER_ON_COMMA.join(Iterables.transform(performanceCounterNames, QuoteStringFunction.INSTANCE))) .add("-SampleInterval") .add("2") // TODO: extract SampleInterval as a config key .add(").CounterSamples") .add("|") .add("Format-Table") .add(String.format("@{Expression={$_.Path};width=%d},@{Expression={$_.CookedValue};width=%(this, job), new SendPerfCountersToSensors(getEntity(), polls), minPeriod); } private static class GetPerformanceCountersJob implements Callable { private final Entity entity; private final String command; GetPerformanceCountersJob(Entity entity, String command) { this.entity = entity; this.command = command; } @Override public WinRmToolResponse call() throws Exception { Maybe machineLocationMaybe = Machines.findUniqueMachineLocation(entity.getLocations(), WinRmMachineLocation.class); if (machineLocationMaybe.isAbsent()) { return null; } WinRmMachineLocation machine = EffectorTasks.getMachine(entity, WinRmMachineLocation.class); WinRmToolResponse response = machine.executePsScript(command); return response; } } @Override @SuppressWarnings("unchecked") protected Poller getPoller() { return (Poller) super.getPoller(); } /** * A {@link java.util.concurrent.Callable} that wraps another {@link java.util.concurrent.Callable}, where the * inner {@link java.util.concurrent.Callable} is executed in the context of a * specific entity. * * @param The type of the {@link java.util.concurrent.Callable}. */ private static class CallInExecutionContext implements Callable { private final Callable job; private AbstractFeed feed; private CallInExecutionContext(AbstractFeed feed, Callable job) { this.job = job; this.feed = feed; } @Override public T call() throws Exception { ExecutionContext executionContext = feed.getExecutionContext(); return executionContext.submit(Maps.newHashMap(), job).get(); } } @VisibleForTesting static class SendPerfCountersToSensors implements PollHandler { private final Entity entity; private final List> polls; private final Set> failedAttributes = Sets.newLinkedHashSet(); private static final Pattern MACHINE_NAME_LOOKBACK_PATTERN = Pattern.compile(String.format("(?<=\\\\\\\\.{0,%d})\\\\.*", OUTPUT_COLUMN_WIDTH)); public SendPerfCountersToSensors(Entity entity, Collection> polls) { this.entity = entity; this.polls = ImmutableList.copyOf(polls); } @Override public boolean checkSuccess(WinRmToolResponse val) { // TODO not just using statusCode; also looking at absence of stderr. // Status code is (empirically) unreliable: it returns 0 sometimes even when failed // (but never returns non-zero on success). if (val == null || val.getStatusCode() != 0) return false; String stderr = val.getStdErr(); if (stderr == null || stderr.length() != 0) return false; String out = val.getStdOut(); if (out == null || out.length() == 0) return false; return true; } @Override public void onSuccess(WinRmToolResponse val) { for (String pollResponse : val.getStdOut().split("\r\n")) { if (Strings.isNullOrEmpty(pollResponse)) { continue; } String path = pollResponse.substring(0, OUTPUT_COLUMN_WIDTH - 1); // The performance counter output prepends the sensor name with "\\" so we need to remove it Matcher machineNameLookbackMatcher = MACHINE_NAME_LOOKBACK_PATTERN.matcher(path); if (!machineNameLookbackMatcher.find()) { continue; } String name = machineNameLookbackMatcher.group(0).trim(); String rawValue = pollResponse.substring(OUTPUT_COLUMN_WIDTH).replaceAll("^\\s+", ""); WindowsPerformanceCounterPollConfig config = getPollConfig(name); Class clazz = config.getSensor().getType(); AttributeSensor attribute = (AttributeSensor) Sensors.newSensor(clazz, config.getSensor().getName(), config.getDescription()); try { Object value = TypeCoercions.coerce(rawValue, TypeToken.of(clazz)); entity.sensors().set(attribute, value); } catch (Exception e) { Exceptions.propagateIfFatal(e); if (failedAttributes.add(attribute)) { log.warn("Failed to coerce value '{}' to {} for {} -> {}", new Object[] {rawValue, clazz, entity, attribute}); } else { if (log.isTraceEnabled()) log.trace("Failed (repeatedly) to coerce value '{}' to {} for {} -> {}", new Object[] {rawValue, clazz, entity, attribute}); } } } } @Override public void onFailure(WinRmToolResponse val) { if (val == null) { log.trace("Windows Performance Counter not executed since there is still now WinRmMachineLocation"); return; } log.error("Windows Performance Counter query did not respond as expected. exitcode={} stdout={} stderr={}", new Object[]{val.getStatusCode(), val.getStdOut(), val.getStdErr()}); for (WindowsPerformanceCounterPollConfig config : polls) { Class clazz = config.getSensor().getType(); AttributeSensor attribute = Sensors.newSensor(clazz, config.getSensor().getName(), config.getDescription()); entity.sensors().set(attribute, null); } } @Override public void onException(Exception exception) { log.error("Detected exception while retrieving Windows Performance Counters from entity " + entity.getDisplayName(), exception); for (WindowsPerformanceCounterPollConfig config : polls) { entity.sensors().set(Sensors.newSensor(config.getSensor().getClass(), config.getPerformanceCounterName(), config.getDescription()), null); } } @Override public String getDescription() { return "" + polls; } @Override public String toString() { return super.toString()+"["+getDescription()+"]"; } private WindowsPerformanceCounterPollConfig getPollConfig(String sensorName) { for (WindowsPerformanceCounterPollConfig poll : polls) { if (poll.getPerformanceCounterName().equalsIgnoreCase(sensorName)) { return poll; } } throw new IllegalStateException(String.format("%s not found in configured polls: %s", sensorName, polls)); } } static class PerfCounterValueIterator implements Iterator { // This pattern matches the contents of the first field, and optionally matches the rest of the line as // further fields. Feed the second match back into the pattern again to get the next field, and repeat until // all fields are discovered. protected static final Pattern splitPerfData = Pattern.compile("^\"([^\\\"]*)\"((,\"[^\\\"]*\")*)$"); private Matcher matcher; public PerfCounterValueIterator(String input) { matcher = splitPerfData.matcher(input); // Throw away the first element (the timestamp) (and also confirm that we have a pattern match) checkArgument(hasNext(), "input "+input+" does not match expected pattern "+splitPerfData.pattern()); next(); } @Override public boolean hasNext() { return matcher != null && matcher.find(); } @Override public String next() { String next = matcher.group(1); String remainder = matcher.group(2); if (!Strings.isNullOrEmpty(remainder)) { assert remainder.startsWith(","); remainder = remainder.substring(1); matcher = splitPerfData.matcher(remainder); } else { matcher = null; } return next; } @Override public void remove() { throw new UnsupportedOperationException(); } } private static enum QuoteStringFunction implements Function { INSTANCE; @Nullable @Override public String apply(@Nullable String input) { return input != null ? "\"" + input + "\"" : null; } } }