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

Add lang parameter to OpenWeathermap input plugin #6504

Merged
merged 6 commits into from
Oct 18, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions plugins/inputs/openweathermap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ Collect current weather and forecast data from OpenWeatherMap.

To use this plugin you will need an [api key][] (app_id).

City identifiers can be found in the [city list][]. Alternately you can
[search][] by name; the `city_id` can be found as the last digits of the URL:
https://openweathermap.org/city/2643743
City identifiers can be found in the [city list][]. Alternately you
can [search][] by name; the `city_id` can be found as the last digits
of the URL: https://openweathermap.org/city/2643743. Language
identifiers can be found in the [lang list][]. Documentation for
condition ID, icon, and main is at [weather conditions][].

### Configuration

Expand All @@ -18,6 +20,9 @@ https://openweathermap.org/city/2643743
## City ID's to collect weather data from.
city_id = ["5391959"]

## Language of the description field.
lang = "en"

## APIs to fetch; can contain "weather" or "forecast".
fetch = ["weather", "forecast"]

Expand All @@ -42,6 +47,8 @@ https://openweathermap.org/city/2643743
- tags:
- city_id
- forecast
- condition_id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swap condition_id with condition_description, making it a tag, and this a field. Also, is the _description more valuable than the _main? It seems like they are essentially duplicate data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be wrong but I think it makes more sense to have ID as a tag and description as a field. Description is the same information as ID, but human readable and localized. I would rather select ID=802 than description="scattered clouds: 25-50%" or possibly a localization like "Mäßig bewölkt". Using ID to select means you don't have to know which language was enabled in the telegraf conf when the data was collected.

Description/ID is more valuable than main. You could map ID to main's value and leave out main completely. I included it for the same reasons as icon- It's part of OWM's API and someone familiar with it would think it's missing if we don't include it.

I made main a tag because it's a single word and it isn't localized. It might make sense to make it a field instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's part of OWM's API and someone familiar with it would think it's missing if we don't include it

It's quite common for telegraf to not create a metric from everything provided by an api. Some things just don't make sense as time series data.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the human readable vs id: telegraf has standardized enum "codes" to be stored as fields and the string representations to be stored as tags. One example is the kube_inventory plugin (state and state_code):

"state_code": stateCode,
"terminated_reason": cs.State.Terminated.GetReason(),
}
tags := map[string]string{
"container_name": *c.Name,
"namespace": *p.Metadata.Namespace,
"node_name": *p.Spec.NodeName,
"pod_name": *p.Metadata.Name,
"state": state,

I would rather select ID=802

Very much agree. I'm not sure where/why you would do the other option. Both are still possible in either configuration from what I understand, but in influx, you can only group by tags.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The influxdb schema design docs have some advice on choosing tags vs fields (https://docs.influxdata.com/influxdb/v1.7/concepts/schema_and_data_layout/). It says to make it a tag if it's commonly queried or used in a group by. Only tags are indexed, so querying on fields is slow.

After reading those docs I think I'd like to leave id and main as tags and description as a field. I imagine querying on and grouping by conditions will be common, so main and id should be tags. If you want the general condition like snowy days, use main="snow". If you want a specific condition like all days with freezing rain, use id="511". I think description isn't going to be commonly queried because it's localized and too long, so it should be a field.

I think the difference between this and kube_inventory is that kube_inventory's state is short (one word), only has three values, and isn't localized. That makes state easier to use than state_code, so it fits the tags criteria better.

I'd be fine leaving out icon but I don't think it's hurting anything. It doesn't give you much over id and main for grouping or querying, so if it stays it should be a field. It's only really useful if you build custom ui and need a weather conditions icon. I'm leaning towards leaving it in and letting people filter it out if they don't want it.

- condition_main
- fields:
- cloudiness (int, percent)
- humidity (int, percent)
Expand All @@ -53,16 +60,20 @@ https://openweathermap.org/city/2643743
- visibility (int, meters, not available on forecast data)
- wind_degrees (float, wind direction in degrees)
- wind_speed (float, wind speed in meters/sec or miles/sec)
- condition_description (string, localized long description)
- condition_icon
reimda marked this conversation as resolved.
Show resolved Hide resolved


### Example Output

```
> weather,city=San\ Francisco,city_id=5391959,country=US,forecast=* cloudiness=40i,humidity=72i,pressure=1013,rain=0,sunrise=1559220629000000000i,sunset=1559273058000000000i,temperature=13.31,visibility=16093i,wind_degrees=280,wind_speed=4.6 1559268695000000000
> weather,city=San\ Francisco,city_id=5391959,country=US,forecast=3h cloudiness=0i,humidity=86i,pressure=1012.03,rain=0,temperature=10.69,wind_degrees=222.855,wind_speed=2.76 1559271600000000000
> weather,city=San\ Francisco,city_id=5391959,country=US,forecast=6h cloudiness=11i,humidity=93i,pressure=1012.79,rain=0,temperature=9.34,wind_degrees=212.685,wind_speed=1.85 1559282400000000000
> weather,city=San\ Francisco,city_id=5391959,condition_id=800,condition_main=Clear,country=US,forecast=* cloudiness=1i,condition_description="clear sky",condition_icon="01d",humidity=35i,pressure=1012,rain=0,sunrise=1570630329000000000i,sunset=1570671689000000000i,temperature=21.52,visibility=16093i,wind_degrees=280,wind_speed=5.7 1570659256000000000
> weather,city=San\ Francisco,city_id=5391959,condition_id=800,condition_main=Clear,country=US,forecast=3h cloudiness=0i,condition_description="clear sky",condition_icon="01n",humidity=41i,pressure=1010,rain=0,temperature=22.34,wind_degrees=249.393,wind_speed=2.085 1570665600000000000
> weather,city=San\ Francisco,city_id=5391959,condition_id=800,condition_main=Clear,country=US,forecast=6h cloudiness=0i,condition_description="clear sky",condition_icon="01n",humidity=50i,pressure=1012,rain=0,temperature=17.09,wind_degrees=310.754,wind_speed=3.009 1570676400000000000
```

[api key]: https://openweathermap.org/appid
[city list]: http://bulk.openweathermap.org/sample/city.list.json.gz
[search]: https://openweathermap.org/find
[lang list]: https://openweathermap.org/current#multi
[weather conditions]: https://openweathermap.org/weather-conditions
163 changes: 96 additions & 67 deletions plugins/inputs/openweathermap/openweathermap.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,21 @@ const (
// The limit of locations is 20.
owmRequestSeveralCityId int = 20

defaultBaseURL = "https://api.openweathermap.org/"
defaultSiteURL = "https://api.openweathermap.org/"
defaultResponseTimeout time.Duration = time.Second * 5
defaultUnits string = "metric"
)

type OpenWeatherMap struct {
AppId string `toml:"app_id"`
CityId []string `toml:"city_id"`
Lang string `toml:"lang"`
Fetch []string `toml:"fetch"`
BaseUrl string `toml:"base_url"`
SiteURL string `toml:"base_url"`
reimda marked this conversation as resolved.
Show resolved Hide resolved
ResponseTimeout internal.Duration `toml:"response_timeout"`
Units string `toml:"units"`

client *http.Client
client *http.Client
baseURL *url.URL
}

var sampleConfig = `
Expand All @@ -46,6 +47,9 @@ var sampleConfig = `
## City ID's to collect weather data from.
city_id = ["5391959"]

## Language of the description field.
# lang = "en"

## APIs to fetch; can contain "weather" or "forecast".
fetch = ["weather", "forecast"]

Expand Down Expand Up @@ -73,12 +77,17 @@ func (n *OpenWeatherMap) Description() string {
}

func (n *OpenWeatherMap) Gather(acc telegraf.Accumulator) error {
var wg sync.WaitGroup
var strs []string

base, err := url.Parse(n.BaseUrl)
if err != nil {
return err
var (
wg sync.WaitGroup
strs []string
err error
reimda marked this conversation as resolved.
Show resolved Hide resolved
)

if n.baseURL == nil {
n.baseURL, err = url.Parse(n.SiteURL)
reimda marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
}

// Create an HTTP client that is re-used for each
Expand All @@ -91,26 +100,16 @@ func (n *OpenWeatherMap) Gather(acc telegraf.Accumulator) error {
n.client = client
}

units := n.Units
switch n.Units {
case "imperial", "standard":
break
case "imperial", "standard", "metric", "":
default:
units = defaultUnits
reimda marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("unknown units: %s", n.Units)
reimda marked this conversation as resolved.
Show resolved Hide resolved
}

for _, fetch := range n.Fetch {
if fetch == "forecast" {
var u *url.URL

for _, city := range n.CityId {
u, err = url.Parse(fmt.Sprintf("/data/2.5/forecast?id=%s&APPID=%s&units=%s", city, n.AppId, units))
if err != nil {
acc.AddError(fmt.Errorf("unable to parse address '%s': %s", u, err))
continue
}

addr := base.ResolveReference(u).String()
addr := n.formatURL("/data/2.5/forecast", city)
wg.Add(1)
go func() {
defer wg.Done()
Expand All @@ -126,21 +125,14 @@ func (n *OpenWeatherMap) Gather(acc telegraf.Accumulator) error {
} else if fetch == "weather" {
j := 0
for j < len(n.CityId) {
var u *url.URL
strs = make([]string, 0)
for i := 0; j < len(n.CityId) && i < owmRequestSeveralCityId; i++ {
strs = append(strs, n.CityId[j])
j++
}
cities := strings.Join(strs, ",")

u, err = url.Parse(fmt.Sprintf("/data/2.5/group?id=%s&APPID=%s&units=%s", cities, n.AppId, units))
if err != nil {
acc.AddError(fmt.Errorf("Unable to parse address '%s': %s", u, err))
continue
}

addr := base.ResolveReference(u).String()
addr := n.formatURL("/data/2.5/group", cities)
wg.Add(1)
go func() {
defer wg.Done()
Expand Down Expand Up @@ -226,6 +218,12 @@ type WeatherEntry struct {
Lon float64 `json:"lon"`
} `json:"coord"`
Visibility int64 `json:"visibility"`
Weather []struct {
ID int64 `json:"id"`
Main string `json:"main"`
Description string `json:"description"`
Icon string `json:"icon"`
} `json:"weather"`
}

type Status struct {
Expand Down Expand Up @@ -253,27 +251,34 @@ func gatherWeatherUrl(r io.Reader) (*Status, error) {
func gatherWeather(acc telegraf.Accumulator, status *Status) {
for _, e := range status.List {
tm := time.Unix(e.Dt, 0)
acc.AddFields(
"weather",
map[string]interface{}{
"cloudiness": e.Clouds.All,
"humidity": e.Main.Humidity,
"pressure": e.Main.Pressure,
"rain": e.Rain.Rain3,
"sunrise": time.Unix(e.Sys.Sunrise, 0).UnixNano(),
"sunset": time.Unix(e.Sys.Sunset, 0).UnixNano(),
"temperature": e.Main.Temp,
"visibility": e.Visibility,
"wind_degrees": e.Wind.Deg,
"wind_speed": e.Wind.Speed,
},
map[string]string{
"city": e.Name,
"city_id": strconv.FormatInt(e.Id, 10),
"country": e.Sys.Country,
"forecast": "*",
},
tm)

fields := map[string]interface{}{
"cloudiness": e.Clouds.All,
"humidity": e.Main.Humidity,
"pressure": e.Main.Pressure,
"rain": e.Rain.Rain3,
"sunrise": time.Unix(e.Sys.Sunrise, 0).UnixNano(),
"sunset": time.Unix(e.Sys.Sunset, 0).UnixNano(),
"temperature": e.Main.Temp,
"visibility": e.Visibility,
"wind_degrees": e.Wind.Deg,
"wind_speed": e.Wind.Speed,
}
tags := map[string]string{
"city": e.Name,
"city_id": strconv.FormatInt(e.Id, 10),
"country": e.Sys.Country,
"forecast": "*",
}

if len(e.Weather) > 0 {
fields["condition_description"] = e.Weather[0].Description
fields["condition_icon"] = e.Weather[0].Icon
tags["condition_id"] = strconv.FormatInt(e.Weather[0].ID, 10)
tags["condition_main"] = e.Weather[0].Main
}

acc.AddFields("weather", fields, tags, tm)
}
}

Expand All @@ -286,20 +291,23 @@ func gatherForecast(acc telegraf.Accumulator, status *Status) {
}
for i, e := range status.List {
tm := time.Unix(e.Dt, 0)
fields := map[string]interface{}{
"cloudiness": e.Clouds.All,
"humidity": e.Main.Humidity,
"pressure": e.Main.Pressure,
"rain": e.Rain.Rain3,
"temperature": e.Main.Temp,
"wind_degrees": e.Wind.Deg,
"wind_speed": e.Wind.Speed,
}
if len(e.Weather) > 0 {
fields["condition_description"] = e.Weather[0].Description
fields["condition_icon"] = e.Weather[0].Icon
tags["condition_id"] = strconv.FormatInt(e.Weather[0].ID, 10)
tags["condition_main"] = e.Weather[0].Main
}
tags["forecast"] = fmt.Sprintf("%dh", (i+1)*3)
acc.AddFields(
"weather",
map[string]interface{}{
"cloudiness": e.Clouds.All,
"humidity": e.Main.Humidity,
"pressure": e.Main.Pressure,
"rain": e.Rain.Rain3,
"temperature": e.Main.Temp,
"wind_degrees": e.Wind.Deg,
"wind_speed": e.Wind.Speed,
},
tags,
tm)
acc.AddFields("weather", fields, tags, tm)
}
}

Expand All @@ -310,8 +318,29 @@ func init() {
}
return &OpenWeatherMap{
reimda marked this conversation as resolved.
Show resolved Hide resolved
ResponseTimeout: tmout,
Units: defaultUnits,
BaseUrl: defaultBaseURL,
SiteURL: defaultSiteURL,
}
})
}

func (n *OpenWeatherMap) formatURL(path string, city string) string {
v := url.Values{
"id": []string{city},
"APPID": []string{n.AppId},
}

if n.Units != "" {
reimda marked this conversation as resolved.
Show resolved Hide resolved
v["units"] = []string{n.Units}
}

if n.Lang != "" {
reimda marked this conversation as resolved.
Show resolved Hide resolved
v["lang"] = []string{n.Lang}
}

relative := &url.URL{
Path: path,
RawQuery: v.Encode(),
}

return n.baseURL.ResolveReference(relative).String()
}
Loading