GithubHelp home page GithubHelp logo

balena-labs-projects / connector Goto Github PK

View Code? Open in Web Editor NEW
15.0 3.0 8.0 231 KB

Auto-configured data connector block based on Telegraf

Python 92.48% Shell 7.52%
telegraf data-sink http-listener broker balenablock

connector's Introduction

balena-blocks/connector

balena

Intelligently connect data sources with data sinks in block-based balena applications. The connector block is a docker image that runs telegraf and code to find other services running on the device, and intelligently connect them.

Features

  • Automatically finds HTTP data sources on the device
  • Automatically finds supported data storage services on the device
  • Configurable HTTP listener
  • Configurable external HTTP data source(s)
  • Configurable HTTP output
  • Device metric support (CPU and Memory usage)

Usage

docker-compose file

To use this image, create a container in your docker-compose.yml file as shown below:

version: '2.1'

services:
  connector:
    image: bh.cr/balenalabs/connector-<arch>
    restart: always
    labels:
      io.balena.features.balena-api: '1' # necessary to discover services
      io.balena.features.supervisor-api: 1  # necessary to discover services in local mode
    privileged: true # necessary to change container hostname
    ports:
      - "8080" # only necessary if using ExternalHttpListener (see below)

You can also set your docker-compose.yml to build a dockerfile.template file, and use the build variable %%BALENA_ARCH%% so that the correct image is automatically built for your device type (see supported devices):

docker-compose.yml:

version: '2.1'

services:
  connector:
    build: ./
    restart: always
    labels:
      io.balena.features.balena-api: '1' # necessary to discover services
      io.balena.features.supervisor-api: 1  # necessary to discover services in local mode
    privileged: true # necessary to change container hostname
    ports:
      - "8080" # only necessary if using ExternalHttpListener (see below)

dockerfile.template

FROM bh.cr/balenalabs/connector-%%BALENA_ARCH%%

Supported devices

The connector block has been tested to work on the following devices:

Device Type Status
Raspberry Pi 3b+
Raspberry Pi 4
Intel NUC
Generic AMD64

Data Sources

Internal HTTP

This type of data source runs it's own HTTP server and provides data readings as json strings. The service must expose port 7575 like this:

  sensor:
    build: ./sensor
    expose:
      - '7575'

The connector block will find this service and configure telegraf to periodically pull from it via HTTP.

The default timeout for retrieving data is 2 seconds. You can change this by setting INTERNAL_HTTP_TIMEOUT to the number of seconds (e.g. 4).

MQTT

By adding an MQTT broker to an application, you can push data into the connector block. Add your broker such as:

mqtt:
    image: arm32v6/eclipse-mosquitto
    ports:
      - "1883:1883"
    restart: always

As long as you call the service mqtt the connector block will automatically find it and configure telegraf to pull data from the broker. Ensure the data is formatted as json strings. Telegraf will be configured to only pull from the sensors topic, so any other data you may wish to put onto the MQTT broker will not be stored (e.g. control or signalling messages).

Example code:

client = mqtt.Client("1")
client.connect("localhost")

while(True):
    value = GetReading() # code omitted for brevity
    client.publish("sensors",json.dumps(value))
    time.sleep(5)

String fields

By default any string fields recieved from MQTT are ignored. For any fields you want to be brought in you will need to specify them in a variable called MQTT_INPUT_STRINGS_FIELDS as a comma-separated list. See the section HTTP section below for a worked example.

External HTTP Pull

This type of source is pulled from a provide via the internet. It is enabled by adding an environment variable to the connector service called EXTERNAL_HTTP_PULL_URL and setting it to the URL of the source:

alt text

Setting the vaiable EXTERNAL_HTTP_PULL_NAME (as above) allows you to rename the resulting data source, otherwise it will appear in your data sinks (see below) as inputs.http.

Headers

Some HTTP APIs that you might like to use with EXTERNAL_HTTP_PULL will require authorization. For that reason you can pass additional parameters using the format EXTERNAL_HTTP_PULL_HEADER_<header-name>. For example: EXTERNAL_HTTP_PULL_HEADER_Authorization could be set to Basic: YWxhZGRpbjpvcGVuc2VzYW1l.

String fields

By default any string fields recieved from a HTTP API are ignored. For any fields you want to be brought in you will need to specify them in a variable called EXTERNAL_HTTP_PULL_STRINGS_FIELDS as a comma-separated list. Here's a worked example:

Say my weather HTTP API brings in the following JSON:

{
   "timezone":3600,
   "id":2643743,
   "name":"London",
   "cod":200
    "sys":{
      "type":1,
      "id":1414,
      "country":"GB",
      "sunrise":1597726289,
      "sunset":1597778246
   },
   "weather":[
      {
         "id":802,
         "main":"Clouds",
         "description":"scattered clouds",
         "icon":"03d"
      }
   ]  
}

In that example, to bring in the "name" field so that "London" appears in my data, I need to add name to my EXTERNAL_HTTP_PULL_STRINGS_FIELDS variable.
However, because the "country" element is nested within the "sys" element, I need to using some notation to specify that JSON path, like this sys_country.
Notice also that the "weather" element has an array of nested elements, including a description of the weather. To get that description I'll need to specify the path (like above), but I also need to specify the index of the array element, in this case "0". So I'll add weather_0_description to my environment variable. All together that will look like this:

alt text

External HTTP PUSH

This type of data source pushes to your device. It is configured by enabling a built-in HTTP listener with the environment variable ENABLE_EXTERNAL_HTTP_LISTENER set to 1:

alt text

Again, the resulting data source can be given a custom name (as above) by setting the EXTERNAL_HTTP_LISTENER_NAME variable.

Additionally, you sometimes need to specify a json_query path - which effectively limits the portion of the JSON document being parsed. This path can be specified with the EXTERNAL_HTTP_LISTENER_JSON_QUERY variable.

Device Metrics

This data source provides the in-built telegraf metrics for the CPU and Memory usage of the device. It is enabled by setting the environment variable ENABLE_DEVICE_METRICS to 1.

This data source is useful for testing connector or simply to allow device resource monitoring as part of your application.

Data Sinks

InfluxDB

Adding an influx timeseries database to your application will cause the connector block to configure telegraf to push data into it. You must name the service influxdb for it to be automatically discovered, such as:

influxdb:
    image: influxdb@sha256:73f876e0c3bd02900f829d4884f53fdfffd7098dd572406ba549eed955bf821f
    container_name: influxdb
    restart: always

By default the database used will be called balena, however you can set a custom database name by setting the INFLUXDB_DB environment variable.

HTTP Data Sink

This data sink will send the data to a URL specified with the environment variable HTTP_PUSH_URL.

Azure Monitor

This data sink pushes data into the Azure Monitor service, using an Azure Application Insights account. In order to use this sink, login (or create) to your Microsoft Azure account, create an Application Insights resource and copy the instrumentation key. Guide here: https://docs.microsoft.com/en-us/azure/azure-monitor/app/create-new-resource

Place this key into an environment variable on your balena device called APPLICATION_INSIGHTS_KEY.

You can now view the data by pointing Azure Monitor to your Application Insights account and charting the correct metrics:

alt text

Customisation

Configuring the pull interval

You can change the pull interval for internal and external HTTP endpoints by setting the PULL_INTERVAL env variable. The default value is 10s. For a list of valid inputs, refer to the telegraf documentation.

Extend image configuration

By default the connector block creates a telegraf configuration file from the combination of discovered services and device environment variables. However for custom configurations you can overload the CMD directive, as such:

dockerfile.template

FROM bh.cr/balenalabs/connector-%%BALENA_ARCH%%

COPY customTelegraf.conf .

CMD ["--config customTelegraf.conf"]

This will stop the auto-wiring code from running and cause telegraf to be run purely from the supplied configuration file.

Troubleshooting

You can turn on telegraf debugging by setting the environment variable DEBUG to 1. This turns on debug logging to the console.

connector's People

Contributors

alanb128 avatar balena-ci avatar chrisys avatar dzeri96 avatar mcraa avatar nucleardreamer avatar phil-d-wilson avatar tmigone avatar wasdee avatar wjlove avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

connector's Issues

Customisation feature broken?

Under the "Extended image configuration" section of the readme.md it suggests that you can include your own Telegraf config file by using the dockerfile.template option. Currently this appears to not work due to an issue with how entry.sh generates the final exec command.

Per the readme.md file, using this in the template file:

FROM balenablocks/connector:%%BALENA_MACHINE_NAME%%
COPY customTelegraf.conf .
CMD ["--config customTelegraf.conf"]

generates the following error in the container:

<...snip...>
21.03.22 14:22:41 (-0400)
21.03.22 14:22:41 (-0400) balenaBlocks connector version: 1.1.2
21.03.22 14:22:41 (-0400) Changing hostname to c5ab484
21.03.22 14:22:41 (-0400) flag provided but not defined: -config ./customTelegraf.conf
21.03.22 14:22:41 (-0400) Telegraf, The plugin-driven server agent for collecting and reporting metrics.
21.03.22 14:22:41 (-0400)
21.03.22 14:22:41 (-0400) Usage:
<...snip...>

note the missing dash in front of the config option, should be "--config ./customTelegraf.conf"

Changing the CMD line to: CMD [" --config customTelegraf.conf"] (note the space between the double quote and the first dash) generates a different error:

<...snip...>
21.03.22 14:27:38 (-0400)
21.03.22 14:27:38 (-0400) balenaBlocks connector version: 1.1.2
21.03.22 14:27:38 (-0400) Changing hostname to c5ab484
21.03.22 14:27:38 (-0400) /app/entry.sh: line 27: /app/ --config ./customTelegraf.conf: No such file or directory
21.03.22 14:27:39 (-0400) Service exited 'connector sha256:12372dfc2677cb971debefb074f19408c5ed77b7d10d65cce930745220980e99'
<...snip...>

The option flag is now correct but the command to run appears to be "/app/" and not "./telegraf". I suspect there is something new with how this bit of code below from entry.sh is executed which I do not understand:

<...snip...>

If command starts with an option, prepend telegraf to it

if [[ "${1#-}" != "$1" ]]; then
set -- ./telegraf "$@"
fi

exec "$@"
<...snip...>

Issues with MQTT and localhost

Hello! This issue is about your example configuration for a mqtt client.

The code you have states that you should be able to connect via the localhost keyword:

client = mqtt.Client("1")
client.connect("localhost")

while(True):
value = GetReading() # code omitted for brevity
client.publish("sensors",json.dumps(value))
time.sleep(5)

However, when I try this, I get the following error:

14.06.21 19:15:52 (-0700) co2 Traceback (most recent call last):
14.06.21 19:15:52 (-0700) co2 File "/usr/src/co2.py", line 85, in
14.06.21 19:15:52 (-0700) co2 mqtt_client.connect("localhost", 1883, 60)
14.06.21 19:15:52 (-0700) co2 File "/usr/local/lib/python3.9/site-packages/paho/mqtt/client.py", line 941, in connect
14.06.21 19:15:52 (-0700) co2 return self.reconnect()
14.06.21 19:15:52 (-0700) co2 File "/usr/local/lib/python3.9/site-packages/paho/mqtt/client.py", line 1075, in reconnect
14.06.21 19:15:52 (-0700) co2 sock = self._create_socket_connection()
14.06.21 19:15:52 (-0700) co2 File "/usr/local/lib/python3.9/site-packages/paho/mqtt/client.py", line 3546, in _create_socket_connection
14.06.21 19:15:52 (-0700) co2 return socket.create_connection(addr, source_address=source, timeout=self._keepalive)
14.06.21 19:15:52 (-0700) co2 File "/usr/local/lib/python3.9/socket.py", line 843, in create_connection
14.06.21 19:15:52 (-0700) co2 raise err
14.06.21 19:15:52 (-0700) co2 File "/usr/local/lib/python3.9/socket.py", line 831, in create_connection
14.06.21 19:15:52 (-0700) co2 sock.connect(sa)
14.06.21 19:15:52 (-0700) co2 OSError: [Errno 99] Cannot assign requested address

Cannot autodetect sensor service

I'm running a device with the sensor service from balenaSense, I've changed the port to 7575 in the code, tried EXPOSE 7575 in the Dockerfile, and tried setting the port in the compose file. The service is accessible on port 7575 externally (which I don't actually want in this case), but it isn't automatically detected by transport. Could this be because my device is in local mode?

Screenshot 2020-07-28 at 10 55 43

MQTT input plugin config format needs updating

10.02.21 10:52:17 (+0000) connector 2021-02-10T10:52:17Z W! [inputs.mqtt_consumer] Server "mqtt:1883" should be updated to use scheme://host:port format
10.02.21 10:52:17 (+0000) connector 2021-02-10T10:52:17Z I! [inputs.mqtt_consumer] Connected [mqtt:1883]

Panics on a fresh device in local mode

[Logs]    [10/5/2020, 7:13:35 PM] Restarting service 'connector sha256:5c237eec5a5eed3dc95ac86b8abdd25370976d5da59c928fc16045e7e67f0594'
[Logs]    [10/5/2020, 7:12:33 PM] [connector] Traceback (most recent call last):
[Logs]    [10/5/2020, 7:12:33 PM] [connector]   File "/root/.local/lib/python3.5/site-packages/balena/models/release.py", line 98, in get
[Logs]    [10/5/2020, 7:12:33 PM] [connector]     rt = self.__get_by_raw_query(raw_query)
[Logs]    [10/5/2020, 7:12:33 PM] [connector]   File "/root/.local/lib/python3.5/site-packages/balena/models/release.py", line 75, in __get_by_raw_query
[Logs]    [10/5/2020, 7:12:33 PM] [connector]     raise exceptions.ReleaseNotFound(raw_query)
[Logs]    [10/5/2020, 7:12:33 PM] [connector] balena.exceptions.ReleaseNotFound: $filter=startswith(commit, 'None')
[Logs]    [10/5/2020, 7:12:33 PM] [connector]
[Logs]    [10/5/2020, 7:12:33 PM] [connector] During handling of the above exception, another exception occurred:
[Logs]    [10/5/2020, 7:12:33 PM] [connector]
[Logs]    [10/5/2020, 7:12:33 PM] [connector] Traceback (most recent call last):
[Logs]    [10/5/2020, 7:12:33 PM] [connector]   File "./autowire.py", line 82, in <module>
[Logs]    [10/5/2020, 7:12:33 PM] [connector]     config = autowire.GetConfig()
[Logs]    [10/5/2020, 7:12:33 PM] [connector]   File "./autowire.py", line 48, in GetConfig
[Logs]    [10/5/2020, 7:12:33 PM] [connector]     services = self.GetServices()
[Logs]    [10/5/2020, 7:12:33 PM] [connector]   File "./autowire.py", line 24, in GetServices
[Logs]    [10/5/2020, 7:12:33 PM] [connector]     release = self.balena.models.release.get(commit)
[Logs]    [10/5/2020, 7:12:33 PM] [connector]   File "/root/.local/lib/python3.5/site-packages/balena/models/release.py", line 105, in get
[Logs]    [10/5/2020, 7:12:33 PM] [connector]     raise exceptions.ReleaseNotFound(commit_or_id)
[Logs]    [10/5/2020, 7:12:33 PM] [connector] balena.exceptions.ReleaseNotFound: None
[Logs]    [10/5/2020, 7:13:35 PM] [connector]
[Logs]    [10/5/2020, 7:13:35 PM] [connector] Changing hostname to aa5da0b
[Logs]    [10/5/2020, 7:13:35 PM] [connector] Generating config
[Logs]    [10/5/2020, 7:13:35 PM] [connector] /root/.local/lib/python3.5/site-packages/pluginbase.py:439: CryptographyDeprecationWarning: Python 3.5 support will be dropped in the next release ofcryptography. Please upgrade your Python.
[Logs]    [10/5/2020, 7:13:35 PM] [connector]   fromlist, level)
[Logs]    [10/5/2020, 7:13:37 PM] [connector] balenablocks/connector
[Logs]    [10/5/2020, 7:13:37 PM] [connector] ----------------------
[Logs]    [10/5/2020, 7:13:37 PM] [connector] Intelligently connecting data sources with data sinks
[Logs]    [10/5/2020, 7:13:38 PM] [connector] Traceback (most recent call last):
[Logs]    [10/5/2020, 7:13:38 PM] [connector]   File "/root/.local/lib/python3.5/site-packages/balena/models/release.py", line 98, in get
[Logs]    [10/5/2020, 7:13:38 PM] [connector]     rt = self.__get_by_raw_query(raw_query)
[Logs]    [10/5/2020, 7:13:38 PM] [connector]   File "/root/.local/lib/python3.5/site-packages/balena/models/release.py", line 75, in __get_by_raw_query
[Logs]    [10/5/2020, 7:13:38 PM] [connector]     raise exceptions.ReleaseNotFound(raw_query)
[Logs]    [10/5/2020, 7:13:38 PM] [connector] balena.exceptions.ReleaseNotFound: $filter=startswith(commit, 'None')
[Logs]    [10/5/2020, 7:13:38 PM] [connector]
[Logs]    [10/5/2020, 7:13:38 PM] [connector] During handling of the above exception, another exception occurred:
[Logs]    [10/5/2020, 7:13:38 PM] [connector]
[Logs]    [10/5/2020, 7:13:38 PM] [connector] Traceback (most recent call last):
[Logs]    [10/5/2020, 7:13:38 PM] [connector]   File "./autowire.py", line 82, in <module>
[Logs]    [10/5/2020, 7:13:38 PM] [connector]     config = autowire.GetConfig()
[Logs]    [10/5/2020, 7:13:38 PM] [connector]   File "./autowire.py", line 48, in GetConfig
[Logs]    [10/5/2020, 7:13:38 PM] [connector]     services = self.GetServices()
[Logs]    [10/5/2020, 7:13:38 PM] [connector]   File "./autowire.py", line 24, in GetServices
[Logs]    [10/5/2020, 7:13:38 PM] [connector]     release = self.balena.models.release.get(commit)
[Logs]    [10/5/2020, 7:13:38 PM] [connector]   File "/root/.local/lib/python3.5/site-packages/balena/models/release.py", line 105, in get
[Logs]    [10/5/2020, 7:13:38 PM] [connector]     raise exceptions.ReleaseNotFound(commit_or_id)
[Logs]    [10/5/2020, 7:13:38 PM] [connector] balena.exceptions.ReleaseNotFound: None
[Logs]    [10/5/2020, 7:13:39 PM] Service exited 'connector sha256:5c237eec5a5eed3dc95ac86b8abdd25370976d5da59c928fc16045e7e67f0594'

ModuleNotFound on raspberry pi 4

It looks like python is not reading packages under /root/.local. I get this error when running autowire

root@cceab6e:~# balena run --rm -ti --privileged balenablocks/connector sh
/app # python3 autowire.py
Traceback (most recent call last):
  File "/app/autowire.py", line 6, in <module>
    from pluginbase import PluginBase
ModuleNotFoundError: No module named 'pluginbase'

This seems to work on pi 3

Implement debug option

The block is currently a black box after generating the config. If, for example, you push data to MQTT but it's not hitting your data sink - there's no way of knowing why. Implement debug logging of some sort.

Create an Azure IOT Hub output interim solution

The telegraf Azure IOT Hub output plugin is await further development, and may be some time before it's usable. Develop a way to get data to Azure via IOT Hub, with no effort from the Edge developer.

Changing MQTT Topics

Currently when autowire finds a MQTT server with the name "mqtt" it connects as a data input source using both "sensors/#" and "balena/#" as topics. Consider allowing the user to add/change/delete topics w/o having to disable autowire and using a custom telegraf.conf file.

Make an assumption about a default input

I started up an app with transport, influx and grafana, which couldn't autowire any inputs (because there are none). Should we fall back to enabling something by default?

Screenshot 2020-07-24 at 17 27 53

Connector suddenly failing with Unauthorized

This was working correctly until yesterday when it suddenly started getting the following error. As far as I know, nothing had changed on the device. It might have lost power at some point, so potentially things got corrupt but this failure seems like maybe something changed on the balena API?

22.11.22 18:43:28 (+0100)  connector  Traceback (most recent call last):
22.11.22 18:43:28 (+0100)  connector    File "/app/./autowire.py", line 96, in <module>
22.11.22 18:43:28 (+0100)  connector      config = autowire.GetConfig()
22.11.22 18:43:28 (+0100)  connector    File "/app/./autowire.py", line 62, in GetConfig
22.11.22 18:43:28 (+0100)  connector      services = self.GetServices()
22.11.22 18:43:28 (+0100)  connector    File "/app/./autowire.py", line 33, in GetServices
22.11.22 18:43:28 (+0100)  connector      device = self.balena.models.device.get_with_service_details(device_id, True)
22.11.22 18:43:28 (+0100)  connector    File "/root/.local/lib/python3.9/site-packages/balena/models/device.py", line 309, in get_with_service_details
22.11.22 18:43:28 (+0100)  connector      raw_data = self.base_request.request(
22.11.22 18:43:28 (+0100)  connector    File "/root/.local/lib/python3.9/site-packages/balena/base_request.py", line 197, in request
22.11.22 18:43:28 (+0100)  connector      raise exceptions.RequestError(response._content)
22.11.22 18:43:28 (+0100)  connector  balena.exceptions.RequestError: b'Unauthorized'

Add ability to specify where ExternalHTTPListener data goes

Currently, when using the External HTTP Listener plugin, data posted to this endpoint from multiple sources ends up in the same place within InfluxDB. This works if you have two different data sources, but if you had two sources the same (two identical sensors in different locations, for example), that post the same data structure, it won't be possible to differentiate between the two.

Facilitate input of strings and boolean values via JSON

This applies at the least to HTTP pull and push, unsure about other inputs.

If we add something like json_string_fields = ['string_*', 'boolean_*'] to the telegraf configuration it means fields in the received JSON with these prefixes will be allowed to pass to the database, without this, strings and boolean values are dropped indiscriminately.

Consider allowing custom telegraf config

Currently you can provide a custom telegraf.conf file by extending the block image but that disables autowire feature. Consider allowing both behaviours simultaneously.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.