Designer configuration
Designer configuration area contains configs for web application interface, security, various UI settings, database and more. Check configuration areas to understand where Designer configuration should be placed in the Nussknacker configuration.
The defaults of the Designer configuration are built into the Designer, in defaultDesignerConfig.conf; the tables below give the default of each setting.
License key
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| licenseKey | High | string | License key received from Nussknacker; unset means no provider component is unlocked. Learn more |
Web interface configuration
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| http.port | High | int | 8080 | HTTP Port of Designer app |
| http.interface | High | string | "0.0.0.0" | HTTP interface for Designer app |
| http.publicPath | Medium | string | "" | if Designer used with reverse proxy and custom path, use this configuration to generate links in Designer properly (Designer app is always served from root path) |
| ssl.enabled | Medium | boolean | false | Should Designer app be served with SSL |
| ssl.keyStore.location | Medium | string | Keystore file location (required if SSL enabled) | |
| ssl.keyStore.password | Medium | string | Keystore file password (required if SSL enabled) | |
| pekko.* | Medium | Pekko HTTP is used for HTTP serving, you can configure it in standard way. Below we give Nussknacker-specific defaults | ||
| pekko.http.server.parsing.max-content-length | Low | int | 300000000 | Requests (e.g. with test data) can be quite large, so we increase the limit |
| pekko.http.server.request-timeout | Low | duration | 1 minute | Consider increasing the value if you have large test data or long savepoint times during deploy |
Database configuration
Database is used to store scenario definitions; data processed by Nussknacker are not stored. Currently, Nussknacker supports following databases:
- HSQL (embedded), we use
syntax_oraoption - PostgreSQL
Please check Slick documentation for detailed list of configuration options.
The table below presents most important options, or the ones that have Nussknacker specific defaults.
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| db.url | High | string | "jdbc:hsqldb:file:"${storageDir}"/db;sql.syntax_ora=true" | Default HSQL location |
| db.driver | High | string | "org.hsqldb.jdbc.JDBCDriver" | "org.postgresql.Driver" in case of PostgreSQL |
| db.user | High | string | "SA" | |
| db.password | High | string | "" | |
| db.connectionTimeout | Low | int | 30000 | |
| db.maximumPoolSize | Low | int | 5 | We have lower limits than default config, since then Designer is not heavy-load application |
| db.minimumIdle | Low | int | 1 | We have lower limits than default config, since then Designer is not heavy-load application |
| db.numThreads | Low | int | 5 | We have lower limits than default config, since then Designer is not heavy-load application |
High Availability configuration
High Availability (HA) mode lets you run several Designer instances concurrently. It is opt-in and disabled
by default. When enabled it requires PostgreSQL as the Designer database shared
by all instances. For an operational overview - how leader
election works, deployment topology and the /api/app/leader endpoint - see the
High Availability operations guide.
Example configuration:
ha {
enabled: true
instanceId: "designer-1"
leader {
heartbeatInterval: 10s
leaseDuration: 30s
releaseOnStop: true
}
periodicLockDuration: 5m
lockQueryTimeout: 5s
deploymentTargetsSynchronizationInterval: 10s
}
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| ha.enabled | High | boolean | false | Enables HA mode. When enabled, PostgreSQL is required. |
| ha.instanceId | High | string | hostname | Identifier of this instance, used to identify the lock holder during leader election. Must be unique across all instances - two instances sharing the same value can both consider themselves the leader. Should also be stable across restarts so that an instance can re-acquire its own lock when ha.leader.releaseOnStop is false. Defaults to the machine hostname; rely on the default only when the hostname is guaranteed unique and stable, otherwise set it explicitly. |
| ha.leader.heartbeatInterval | Medium | duration | 10s | How often the leader renews its lock. |
| ha.leader.leaseDuration | Medium | duration | 30s | How long a leader lock stays valid without renewal. Must be greater than heartbeatInterval (recommended: at least 3 * heartbeatInterval). Roughly determines how long coordinated background work can pause during failover. |
| ha.leader.releaseOnStop | Low | boolean | true | When true, the leader releases its lock on graceful shutdown so another instance can take over immediately. Set to false to let the same instance re-acquire its lock on restart without waiting for the lease to expire. |
| ha.periodicLockDuration | Low | duration | 5m | Maximum time a periodic-scenario deployment lock is held (covers the full deploy round-trip). |
| ha.lockQueryTimeout | Low | duration | 5s | Timeout for individual lock database queries. Must be shorter than ha.leader.heartbeatInterval. |
| ha.deploymentTargetsSynchronizationInterval | Low | duration | 10s | How often each instance refreshes its deployment targets created over the API from the database. Roughly how long it takes for a target created, changed or removed on one instance to be seen by the others. |
Metrics settings
Metric dashboard
Each scenario can have a link to Grafana dashboard.
The Docker Compose installation comes with a sample nussknacker-scenario dashboard (grafana/dashboards). You can modify/configure your own, the only assumption that
we make is that variable scenarioName is used to display
metrics for particular scenario.
Each scenario type can have different dashboard, this is configured by
metricsSettings.scenarioTypeToDashboard settings. If no mapping is configured, metricsSettings.defaultDashboard is used.
Actual link for particular scenario is created by replacing
$dashboardwith configured dashboard$scenarioNamewith scenario name inmetricsSettings.urlsetting.
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| metricsSettings.url | High | string | /grafana/d/$dashboard?theme=dark&var-scenarioName=$scenarioName&var-env=local (for Docker setup) | URL (accessible from user browser, in docker setup its configured as relative URL) to Grafana dashboard, see above for details |
| metricsSettings.defaultDashboard | Medium | string | nussknacker-scenario (for Docker setup) | Default dashboard |
| metricsSettings.scenarioTypeToDashboard | Low | map | Mapping of scenario types to dashboard |
Signing in to the dashboard
The dashboard is opened in an iframe, with the user's credentials in its query parameters:
- a user signed in with single sign-on (OAuth2/OIDC) gets their access token in
auth_token, which Grafana can check itself with its JWT authentication (url_login = true), - a user signed in with a password never has it handed out. With the embed token extension on the Designer's
classpath (
extensions/embed-token/*), they get a token innuEmbedTokeninstead, which tells who the user is and nothing else: it is valid for 60 seconds, can be checked only once and does not authenticate any other request to the Designer. A proxy in front of Grafana checks it withGET /api/custom/embedToken/verifyand the token in anAuthorization: Bearerheader. The answer is204with the user's identity, username, whether they are an admin and their roles (comma-separated) in theNu-User-Id,Nu-User-Username,Nu-User-Is-AdminandNu-User-Rolesheaders, or401for a token that is unknown, expired or already used. The proxy then signs the user in to Grafana with its auth proxy (enable_login_token = true). Grafana starts a session for such a user only on its/loginpage, so the proxy sends the request with a valid token there, and turns Grafana's redirect back to the dashboard.
The Metrics tab always asks for an embed token; another iframe tab does with embedTokenInQuery (see
Main menu configuration). Without the extension on the classpath the page is opened without a token.
For example, with nginx in front of Grafana served from /grafana:
http {
map $nu_embed_roles $nu_embed_grafana_user {
"~(^|,)(Admin|Reader)(,|$)" "true";
default "";
}
map $nu_embed_grafana_user $grafana_webauth_user {
"true" "nussknacker:$nu_embed_user_id";
default "";
}
map $nu_embed_grafana_user $grafana_embed_uri {
"true" "/grafana/login";
default $request_uri;
}
map $nu_embed_grafana_user $grafana_embed_cookie {
"true" "";
default $http_cookie;
}
map $request_uri $nu_embed_redirect {
"~^(?<nu_embed_path>[^?]*)\?(?<nu_embed_before>.*?)&?nuEmbedToken=[^&]*(?<nu_embed_after>.*)$" "$nu_embed_path?$nu_embed_before$nu_embed_after";
default $request_uri;
}
upstream grafana {
server grafana:3000;
}
server {
absolute_redirect off;
location /grafana/ {
if ($arg_nuEmbedToken != "") {
error_page 418 = @nu_embed_sign_in;
return 418;
}
# Always set, so that a client cannot send a user of its own
proxy_set_header X-WEBAUTH-USER "";
proxy_pass http://grafana/grafana/;
}
location @nu_embed_sign_in {
set $nu_embed_token $arg_nuEmbedToken;
auth_request /_nu_embed_token;
auth_request_set $nu_embed_user_id $upstream_http_nu_user_id;
auth_request_set $nu_embed_roles $upstream_http_nu_user_roles;
proxy_set_header X-WEBAUTH-USER $grafana_webauth_user;
proxy_set_header Cookie $grafana_embed_cookie;
proxy_redirect ~^/grafana/$ $nu_embed_redirect;
proxy_pass http://grafana$grafana_embed_uri;
}
location = /_nu_embed_token {
internal;
proxy_method GET;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Authorization "Bearer $nu_embed_token";
proxy_intercept_errors on;
error_page 400 401 403 404 405 500 502 503 504 = @nu_embed_token_rejected;
proxy_pass http://designer:8080/api/custom/embedToken/verify;
}
location @nu_embed_token_rejected {
return 204;
}
}
}
Here users with the Admin or Reader role are signed in to Grafana (as the role Grafana assigns new users,
users.auto_assign_org_role), and a token that is rejected signs nobody in but leaves Grafana's
own session or anonymous access in place.
The tokens are kept in the Designer's memory, so a highly available installation using them has to run the Designer active-passive: the proxy's check then always reaches the replica that issued the token.
Counts
Counts are based on InfluxDB metrics, stored in nodeCount measurement by default.
countsSettings.queryMode setting can be used to choose metric computation algorithm:
OnlySingleDifference- subtracts values between end and beginning of requested range. Fast method, but if restart in the requested time range is detected error is returned. We assume the job was restarted when event counter at the source decreases.OnlySumOfDifferences- difference is computed by summing differences in measurements for requested time range. This method works a bit better for restart situations, but can be slow for large diagrams and wide time ranges.SumOfDifferencesForRestarts- if restart is detected, the metrics are computed withOnlySumDifferences, otherwise - withOnlySingleDifferences
If you have custom metrics settings which result in different fields or tags (e.g. you have different telegraf configuration), you can configure required values with the settings presented below:
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| countsSettings.influxUrl | Medium | string | Main InfluxDB query endpoint (e.g. http://influx:8086/query). It should be accessible from Nussknacker Designer server, not from user browser | |
| countsSettings.database | Medium | string | InfluxDB database name | |
| countsSettings.user | Medium | string | User name (optional) | |
| countsSettings.password | Medium | string | Password (optional) | |
| countsSettings.additionalHeaders | Low | map of strings | empty map | Additional headers sent to InfluxDB |
| countsSettings.queryMode | Low | OnlySingleDifference / OnlySumOfDifferences / SumOfDifferencesForRestarts | OnlySumOfDifferences | |
| countsSettings.metricsConfig.sourceCountMetric | Low | string | source_count | |
| countsSettings.metricsConfig.nodeCountMetric | Low | string | nodeCount | |
| countsSettings.metricsConfig.nodeIdTag | Low | string | nodeId | |
| countsSettings.metricsConfig.additionalGroupByTags | Low | list of string | ["slot", "instanceId"] | |
| countsSettings.metricsConfig.scenarioTag | Low | string | scenario | |
| countsSettings.metricsConfig.countField | Low | string | count | |
| countsSettings.metricsConfig.envTag | Low | string | env |
Deployment settings
Nussknacker Designer can be configured to replace certain values in comments to links that can point e.g. to external issue tracker like
GitHub issues or Jira. For example, MARKETING-555 will change to link https://jira.organization.com/jira/browse/MARKETING-555.
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| commentSettings.substitutionPattern | Low | regexp | Regular expression to look for issue identifier (e.g. (issues/[0-9]*) - note use of regexp group) | |
| commentSettings.substitutionLink | Low | string | Link template (e.g. https://git.example.com/project/$1 - $1 will be replaced with matched group from substitutionPattern config | |
| deploymentCommentSettings.validationPattern | Low | regexp | If deploymentCommentSettings is specified, comment matching validation pattern is required for deployment. Also, if substitutionPattern is defined, at least one match is required | |
| deploymentCommentSettings.exampleComment | Low | string | Example of comment which passes validation. Unlike validationPattern field is not mandatory. |
Security
Overview
Nussknacker has pluggable security architecture - we support three types of authentication: BasicAuth, OAuth2 and OpenID Connect (OIDC). Configuration specific to each of these three types of authentication mechanism is described in the dedicated sections.
Nussknacker supports roles; the roles permissions are defined in the users configuration file.
Users, roles and permissions
Each user has id and set of permissions for every scenario category. The following permissions are supported:
- Read - user can view scenarios in category
- Write - user can modify/add new scenarios in category
- Deploy - user can deploy or cancel scenarios in given category
You can set isAdmin flag to a certain role in the users configuration file.
Users who have this role will be considered an Admin user by the system and will have all the permissions to every
scenario category as well as all the global permissions.
Global permissions
In addition to permission system oriented around scenarios categories we provide additional set of permissions. This feature is designed to control access to components that have no category attached, or it doesn't make sense for them to have one.
Currently supported permissions:
- Impersonate - Enables technical user to impersonate a business user and act on their behalf with their permissions.
Impersonation
Nussknacker supports an impersonation mechanism on the API level, allowing system's technical users to perform actions on behalf of
business users. It is off unless authorization.impersonateActionEnabled is true; with it on, a technical user has
to have the Impersonate global permission configured in order to be able to impersonate.
Currently only the BasicAuth and BasicAuthForm security mechanisms support this feature.
You can configure whether Admin users can be impersonated by such technical users with isAdminImpersonationPossible
setting in the authentication section. By default, it's set to false.
Common configuration parameters
The table below contains parameters common to all the supported authentication methods.
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| authentication.method | required | string | BasicAuth, BasicAuthForm, Oidc or OAuth2 | |
| authentication.usersFile | required | url or path | URL or path to a file with a mapping of user identities to roles and roles to permissions | |
| authentication.isAdminImpersonationPossible | optional | boolean | false | true or false. Flag describing whether users with Impersonate global permission can impersonate Admin users. |
| authorization.impersonateActionEnabled | optional | boolean | false | true turns impersonation on. Off, nobody impersonates - not an admin, not a role with Impersonate - under every authentication method. |
| authorization.scenarioDemoActionEnabled | optional | boolean | false | true offers the Scenario.Demo action. Off, nobody has it - not an admin, not a role with the Demo permission. |
Users' file format:
The association of the users to the roles is in the users' configuration file; in the case of OIDC it can additionally be supplemented by the list of roles provided by the OpenId provider.
If OpenID Connect (OIDC) authentication is used, the information about the user identity is stored in the field sub (subject) of the OIDC token - make sure that these values match.
users: [
{
identity: "admin"
roles: ["Admin"]
},
{
identity: "userId",
username: "overrided username",
roles: ["Admin"]
},
{
identity: "userWithAdminTab"
roles: ["User", "UserWithAdminTab"]
}
{
identity: "user"
roles: ["User"]
}
]
rules: [
{
role: "Admin"
isAdmin: true
},
{
role: "UserWithAdminTab"
permissions: ["Read", "Write", "Deploy"]
globalPermissions: ["AdminTab"]
categories: ["Category2", "RequestResponseCategory1"]
},
{
role: "User"
permissions: ["Read", "Write"]
categories: ["Category1", "Category2"]
}
]
BasicAuth security module
In order to use Basic authentication you just need to set authentication.method to BasicAuth and
provide either plain or encrypted passwords for users additionally to the usersFile's content as follows:
users: [
{
...
password: "pass"
},
{
...
encryptedPassword: "$2a$12$oA3U7DXkT5eFkyB8GbtKzuVqxUCU0zDmcueBYV218zO/JFQ9/bzY6"
}
]
...
Encrypted hashes
Encryption of passwords uses BCrypt algorithm. You can generate sample hash using command:
python -c 'import bcrypt; print(bcrypt.hashpw("password".encode("utf8"), bcrypt.gensalt(rounds=12, prefix="2a")))'
If you don't have bcrypt installed, use pip install bcrypt.
Be aware that usage of BCrypt hashes will cause significant CPU overhead for processing of each http request, because we don't have sessions and all requests are authenticated. To avoid this overhead you can configure caching of hashes using configuration:
authentication: {
method: "BasicAuth"
usersFile: "conf/users.conf"
cachingHashes {
enabled: true
maximumSize: 1000
expireAfterAccess: 5 minutes
}
}
Hash caching is disabled by default. The example above retains at most 1,000 entries, expiring after five minutes
of inactivity. Each access refreshes the expiry, so this is not a hard five-minute lifetime from insertion.
It only applies to bcrypt hashes: file-based accounts configured with password instead of encryptedPassword
do not use it and gain no performance improvement.
The Docker Compose installation bundle (examples/installation)
enables these bounded settings and exposes BASIC_AUTH_CACHE_ENABLED=false to disable them.
BasicAuthForm security module
BasicAuthForm authenticates the same users as BasicAuth and takes the same parameters, but the Designer asks for
the username and password on a sign-in page of its own instead of the browser's login dialog, and signing out works
in every browser. API clients keep sending HTTP Basic credentials as with BasicAuth, but they have to send them
with the first request: the 401 challenge lists Bearer before Basic, which keeps the browser's dialog away, and
a client that negotiates the scheme from the challenge would pick Bearer.
It can also sign every visitor in automatically, with no further parameters. The account's password is public, so give it read-only roles:
authentication: {
method: "BasicAuthForm"
usersFile: "conf/users.conf"
autoLogin: {
login: "demo"
password: "demo"
}
}
OpenID Connect (OIDC) security module
When talking about OAuth2 in the context of authentication, most people probably mean OpenID Connect, an identity layer built on top of it. Nussknacker provides a separate authentication provider for OIDC with simple configuration and provider discovery. The only supported flow is the authorization code flow with client secret.
You can select this authentication method by setting the authentication.method parameter to Oidc
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| authentication.issuer | required | url | OpenID Provider's location | |
| authentication.clientId | required | string | Client identifier valid at the authorization server | |
| authentication.clientSecret | required | string | Secret corresponding to the client identifier at the authorization server | |
| authentication.audience | recommended | string | Required aud claim value of an access token that is assumed to be a JWT. | |
| authentication.rolesClaims | recommended | list of string | Name of the field in the ID token which contains list of user roles. These roles must correspond to those defined in the usersFile. | |
| authentication.redirectUri | optional | url | inferred from UI's location | Callback URL to which a user is redirected after successful authentication |
| authentication.scope | optional | string | openid profile | Scope parameter's value sent to the authorization endpoint. |
| authentication.authorizationEndpoint | auxiliary | url or path | discovered | Absolute URL or path relative to Issuer overriding the value retrieved from the OpenID Provider |
| authentication.tokenEndpoint | auxiliary | url or path | discovered | as above |
| authentication.userinfoEndpoint | auxiliary | url or path | discovered | as above |
| authentication.jwksUri | auxiliary | url or path | discovered | as above |
| authentication.tokenCookie.name | auxiliary | string | name of cookie to store access token | |
| authentication.tokenCookie.path | auxiliary | string | path of access token cookie | |
| authentication.tokenCookie.domain | auxiliary | string | domain of access token cookie | |
| authentication.usernameClaim | optional | string | The OIDC claim from JWT which be mapped to the username at Nussknacker authorized user object. Available options: preferred_username, given_name, nickname, name. By default, username is represented by the sub (identifier) claim from JWT. | |
| authentication.enableBasicAuthAccounts | optional | boolean | false | Lets users defined in the usersFile with a password/encryptedPassword authenticate via HTTP Basic in addition to the OIDC flow. Formerly enableBasicAuthForTechnicalAccounts, still read - see below |
| authentication.autoLogin.login | optional | string | Identity of a usersFile account that the frontend signs every visitor in as - see below | |
| authentication.autoLogin.password | optional | string | Password of that account, handed to every visitor | |
| authentication.defaultSignIn | optional | string | SingleSignOn | What someone without a session is offered first: SingleSignOn, Password or Chooser - see below |
Technical accounts with HTTP Basic authentication
Some clients cannot perform the OIDC flow - for example the RemoteEnvironment scenario-migration client, which always
sends HTTP Basic credentials. Setting authentication.enableBasicAuthAccounts to true lets such accounts
authenticate with a password while all regular users keep authenticating through the OIDC provider. The accounts are
plain entries of the same usersFile, with a password or (preferably) an encryptedPassword:
users: [
{
identity: "some-oidc-subject-id"
roles: ["User"]
}
{
identity: "migration-technical-account"
encryptedPassword: "$2a$12$..."
roles: ["Writer"]
}
]
Users without a password - the regular, OIDC-identified ones - cannot authenticate this way. Two cautions:
- give a technical account only the roles it needs (the migration client needs write access to the target environment, not administrative rights),
- make its
identityone that the OpenID Provider cannot issue as a subject (sub) claim. TheusersFilemaps both OIDC subjects and technical accounts byidentity, so an OIDC user whose subject equals a technical account's identity would be granted that account's roles.
Automatic login
An instance can let people look around without an account of their own by signing every visitor in as one shared
account. With Oidc and OAuth2 it needs enableBasicAuthAccounts, because the frontend signs in with HTTP Basic:
authentication: {
method: "Oidc"
enableBasicAuthAccounts: true
autoLogin: {
login: "demo"
password: "demo"
}
}
The account must be a usersFile entry whose identity is the login, with the same password. The password is returned by
GET /authentication/<method>/settings to anyone, so give the account read-only roles. Visitors with an account of
their own sign in by opening the Designer with the ?signin query parameter.
Signing in with a password
enableBasicAuthAccounts also lets people sign in from the UI with an identity and password from the usersFile.
Any account with a password can sign in this way, technical accounts included. Opening the Designer with ?signin
shows a page offering both single sign-on and username and password. What a plain visit without a session does
depends on defaultSignIn:
SingleSignOn(default) - goes to the OpenID Provider,Password- shows the username and password form alone,Chooser- shows the page offering both.
Where automatic login is configured, a plain visit signs in with it instead. Password and
Chooser need enableBasicAuthAccounts; the Designer does not start otherwise.
This works both with the default frontend strategy and with the Remote one set in
overrideFrontendAuthenticationStrategy. The default strategy cannot end the session the OpenID Provider holds, so
after signing out it shows the page in place of SingleSignOn - with single sign-on alone where
enableBasicAuthAccounts is off - and single sign-on chosen there asks the provider for credentials rather than
signing the same person straight back in.
Auth0 sample configuration
Assuming ${nussknackerUrl} is the location of your deployment, in your Auth0 tenant settings do the following:
- Create a "Regular Web Application" with the "Allowed Callback URL's" field set to
${nussknackerUrl} - Create an "API" with the "Identifier" field preferably set to
${nussknackerUrl}/api - Create an Auth Pipeline Rule with the content:
function (user, context, callback) {
const assignedRoles = (context.authorization || {}).roles || ['User'];
let idTokenClaims = context.idToken || {};
idTokenClaims[`nussknacker:roles`] = assignedRoles;
context.idToken = idTokenClaims;
callback(null, user, context);}
You can find more configurations options at Auth0's documentation on applications and APIs
In Nussknacker's configuration file add the following authentication section:
authentication: {
method: "Oidc"
issuer: "https://<the value of Applications -> Application Name -> Settings -> Basic Information -> Domain>"
clientSecret: "<the value of Applications -> Application Name -> Settings -> Basic Information -> Client Secret>"
clientId: "<the value of Applications -> Application Name -> Settings -> Basic Information -> Client Identifier>"
audience: "<the value of APIs -> API Name -> Settings -> General Settings -> Identifier>"
rolesClaims: ["nussknacker:roles"]
usersFile: "conf/users.conf"
}
The role names in the usersFile should match the roles defined in the Auth0 tenant.
MS Azure Active Directory sample configuration
- Open MS Azure Portal: https://portal.azure.com/
- Go to Azure Active Directory Service
- Register new app: AAD Service -> App registrations -> New registration
- Add auth platform: AAD Service -> App registrations -> Your App -> Authentication -> Add a platform
- Register app roles: AAD Service -> App registrations -> Your App -> App roles -> Create app role
- Add client secret: AAD Service -> App registrations -> Your App -> Certificates & secrets -> New client secret
- Configure users roles: Enterprise applications -> Your App -> Users and groups -> Add user/group
In Nussknacker's configuration file add the following authentication section:
authentication: {
method: "Oidc"
issuer: "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0"
clientSecret: <the value of App registrations -> Your App -> Certificates & secrets -> Your created secret value>
clientId: <the value of App registrations -> Your App -> Overview -> Application (client) ID>
usernameClaim: "name" # Here MS AAD returns at JWT full user name
rolesClaims: ["roles"] # Here MS AAD returns at JWT information about assigned roles
usersFile: "conf/users.conf"
}
The value of YOUR_TENANT_ID you can find at App registrations -> Your App -> Directory (tenant) ID. More information about the API you can find at https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration.
The role names in the usersFile should match the roles defined in MS AAD App registrations -> Your App -> App roles.
OAuth2 security module
Generic configuration
authentication: {
method: ${?AUTHENTICATION_METHOD} (default: "BasicAuth")
usersFile: ${?AUTHENTICATION_USERS_FILE}
authorizeUri: ${?OAUTH2_AUTHORIZE_URI}
clientSecret: ${?OAUTH2_CLIENT_SECRET}
clientId: ${?OAUTH2_CLIENT_ID}
issuer: ${?OAUTH2_ISSUER}
profileUri: ${?OAUTH2_PROFILE_URI}
profileFormat: ${?OAUTH2_PROFILE_FORMAT}
accessTokenUri: ${?OAUTH2_ACCESS_TOKEN_URI}
redirectUri: ${?OAUTH2_REDIRECT_URI}
implicitGrantEnabled: ${?OAUTH2_IMPLICIT_GRANT_ENABLED}
jwt {
accessTokenIsJwt: ${?OAUTH2_ACCESS_TOKEN_IS_JWT} (default: false)
userinfoFromIdToken: ${?OAUTH2_USERINFO_FROM_ID_TOKEN} (default: false)
audience: ${?OAUTH2_JWT_AUDIENCE}
publicKey: ${?OAUTH2_JWT_AUTH_SERVER_PUBLIC_KEY}
publicKeyFile: ${?OAUTH2_JWT_AUTH_SERVER_PUBLIC_KEY_FILE}
certificate: ${?OAUTH2_JWT_AUTH_SERVER_CERTIFICATE}
certificateFile: ${?OAUTH2_JWT_AUTH_SERVER_CERTIFICATE_FILE}
idTokenNonceVerificationRequired: ${?OAUTH2_JWT_ID_TOKEN_NONCE_VERIFICATION_REQUIRED}
}
accessTokenParams {
grant_type: ${?OAUTH2_GRANT_TYPE}
}
authorizeParams {
response_type: ${?OAUTH2_RESPONSE_TYPE}
scope: ${?OAUTH2_SCOPE}
audience: ${?OAUTH2_AUDIENCE}
}
usernameClaim: ${?OAUTH2_USERNAME_CLAIM}
headers {
Accept: ${?AUTHENTICATION_HEADERS_ACCEPT}
}
authorizationHeader: "Authorization" (default)
accessTokenRequestContentType: "application/json" (default)
defaultTokenExpirationDuration: "1 hour" (default)
tokenCookie: None (default)
overrideFrontendAuthenticationStrategy: None (default)
enableBasicAuthAccounts: false (default)
autoLogin: None (default)
defaultSignIn: "SingleSignOn" (default)
}
enableBasicAuthAccounts, autoLogin and defaultSignIn work as with Oidc - see
technical accounts, automatic login
and signing in with a password.
When method is set to OAuth2, the following fields are mandatory: clientSecret, clientId, authorizeUri
, redirectUri, accessTokenUri, profileUri, profileFormat, implicitGrantEnabled, usersFile.
For the profileFormat one of oidc or github is supported by default.
Subconfigs accessTokenParams, accessTokenRequestContentType, authorizeParams, headers are optional and every
field from any of the subconfigs is optional and could be provided separately.
By default, access token request is sent using application/json content type, to change it (e.g.
to application/x-www-form-urlencoded) use accessTokenRequestContentType config.
Subconfig jwt is also optional. However, if it is present and enabled is set to
true, the audience and one of the publicKey, publicKeyFile, certificate, certificateFile,
fields have to be provided.
Access tokens are introspected only once and then stored in a cache for the expiration time.
When using JWT, an incoming access token is validated against its signature and aud(audience) claim.
Otherwise, a token is considered valid when it allows a profileUri (e.g. /userinfo) query.
No token refreshing nor revoking is implemented.
Remarks:
-
Setting
jwt.idTokenNonceVerificationRequiredtotruehas an effect only ifimplicitGrantEnabledis also set totrue. -
For security reasons, if implicit flow used,
implicitGrantEnabledandjwt.idTokenNonceVerificationRequiredshould be set totrue. FurthermoreauthorizeParams.response_typeshould be set to"access_token id_token"(not only"access_token"). Then a receivedaccess_tokenformat has to be JWT (likeid_token).Of course, this does not apply when
authorizeParams.response_typeis set tocode(code flow used). -
Provided
jwtis enabled, the backend first checks whether a user profile can be obtained from theaccess_token, secondly it tries to obtain the profile from a request sent toauthorizeUri. -
Access token can be configured to be set in http-only cookie (e.g. for enabling proxy grafana authentication). This is disabled by default, it can be enabled with following config:
authentication {
...
tokenCookie {
name: "cookieName"
path: "/grafana" //optional
domain: "mydomain.com" //optional
}
}
OAuth2 security module - GitHub example with code flow
Configuration example:
authentication: {
method: "OAuth2"
clientSecret: ""
clientId: ""
authorizeUri: "https://github.com/login/oauth/authorize"
redirectUri: "http://localhost:3000"
accessTokenUri: "https://github.com/login/oauth/access_token"
profileUri: "https://api.github.com/user"
profileFormat: "github"
implicitGrantEnabled: false
accessTokenParams {
grant_type: "authorization_code"
}
headers {
Accept: "application/json"
}
authorizeParams {
response_type: "code"
}
usersFile: "./src/test/resources/oauth2-users.conf"
}
Users file example:
users: [ //Special settings by user email
{
identity: "some@email.com"
roles: ["Admin"]
}
]
rules: [
{
role: "Admin"
isAdmin: true
},
{
role: "User" //this is default role for all users
permissions: ["Read", "Write", "Deploy"]
categories: ["Streaming", "FraudDetection"]
}
]
UI customization options
Process Toolbar Configuration
Toolbars and buttons at process window are configurable, you can configure params:
- uuid - optional uuid identifier which determines unique code for FE localstorage cache, default: null - we generate uuid from hashcode config
- topLeft - optional top left panel, default: empty list
- topCenter - optional center top panel, default: empty list
- topRight - optional top right panel, default: empty list
- bottomLeft - optional bottom left panel, default: empty list
- bottomCenter - optional center bottom panel, default: empty list
- bottomRight - optional bottom right panel, default: empty list
- aboveNodeWindow - optional panel shown above an open node window, default: empty list
- dynamicLeft - optional left side panel, default: empty list
- dynamicRight - optional right side panel, default: empty list
Example configuration:
processToolbarConfig {
defaultConfig {
topLeft: [
{ type: "search-panel" }
]
topRight: [
{
id: "actions-panel"
type: "buttons-panel"
title: "actions"
buttons: [
{ type: "process-save", disabled: { fragment: false, archived: true, type: "oneof" } }
{ type: "custom-link", title: "Metrics for $processName", url: "/metrics/$processId" }
]
}
]
}
}
We can also create special configuration for each category by:
processToolbarConfig {
categoryConfig {
"CategoryName" {
id: "58f1acff-d864-4d66-9f86-0fa7319f7043"
topLeft: [
{ type: "creator-panel", hidden: {fragment: true, archived: false, type: "allof"} }
]
}
}
}
Toolbar Panel Conditioning
Configuration allow us to:
- hiding panels
- hiding or disabling buttons
Each of this function can be configured by condition expression where we can use three parameters:
fragment: boolean- if true then condition match only fragment, by default ignoredarchived: boolean- if true then condition match only archived, by default ignoredtype: allof / oneof- information about that checked will be only one condition or all conditions
Toolbar Panel Templating
Configuration allows to templating params like:
name- available only on Buttonstitle- available on Panels and Buttonsurl- available only on CustomLink buttonsicon- available only on Buttons
Right now we allow to template two elements:
- scenario id -
$processId - scenario name -
$processName
Example usage:
title: "Metrics for $processName"name: "deploy $processName"url: "/metrics/$processId"icon: "/assets/process-icon-$processId"
Default Process Panel Configuration
The Scenario panel, with the scenario's name, state, labels, actions, description and validation results, is always
shown first in topRight and is not part of this configuration.
processToolbarConfig {
defaultConfig {
topLeft: []
topRight: [
{ type: "search-panel" }
{ type: "activities-panel" }
]
topCenter: [
{
id: "top-edit-panel"
type: "buttons-panel"
title: "edit"
hidden: { archived: true }
buttonsVariant: "small"
buttons: [
{ type: "edit-undo" }
{ type: "edit-redo" }
{ type: "edit-copy" }
{ type: "edit-paste" }
{ type: "edit-delete" }
]
}
{
id: "top-actions-panel"
type: "buttons-panel"
title: "actions"
hidden: { archived: true }
buttonsVariant: "small"
buttons: [
{ type: "process-save", title: "Save changes", disabled: { archived: true } }
{
type: "scenario-test"
disabled: { archived: true }
docs: { url: "https://docs.nussknacker.io/scenariosAuthoring/testingAndDebugging", label: "docs" }
}
{ type: "process-deploy", disabled: { fragment: true, archived: true, type: "oneof" } }
{ type: "process-redeploy", disabled: { fragment: true, archived: true, type: "oneof" } }
{ type: "process-cancel", disabled: { fragment: true, archived: true, type: "oneof" } }
{ type: "process-run-off-schedule", disabled: { archived: true }, hidden: { fragment: true } }
{ type: "live-data", disabled: { fragment: true, archived: true, type: "oneof" } }
]
}
]
aboveNodeWindow: [
{
id: "above-node-panel"
type: "buttons-panel"
hidden: { archived: true }
additionalParams: { noDrag: "true" }
buttonsVariant: "small"
buttons: [
{ type: "process-save", title: "Save changes", disabled: { archived: true } }
{
type: "scenario-test"
disabled: { archived: true }
docs: { url: "https://docs.nussknacker.io/scenariosAuthoring/testingAndDebugging", label: "docs" }
}
{ type: "process-deploy", disabled: { fragment: true, archived: true, type: "oneof" } }
{ type: "process-redeploy", disabled: { fragment: true, archived: true, type: "oneof" } }
{ type: "process-cancel", disabled: { fragment: true, archived: true, type: "oneof" } }
{ type: "process-run-off-schedule", disabled: { archived: true }, hidden: { fragment: true } }
{ type: "live-data", disabled: { fragment: true, archived: true, type: "oneof" } }
]
}
]
bottomCenter: [
{
id: "view-panel"
type: "buttons-panel"
title: "view"
buttons: [
{ type: "view-zoom-in" }
{ type: "view-zoom-out" }
{ type: "view-reset" }
{ type: "edit-layout" }
]
}
{
id: "metrics-panel"
type: "buttons-panel"
title: "Metrics"
buttons: [
{ type: "scenario-metrics", disabled: { fragment: true } }
{ type: "test-counts" }
{ type: "test-from-file", disabled: { archived: true }, hidden: { fragment: true } }
{ type: "test-generate", disabled: { archived: true }, hidden: { fragment: true } }
{ type: "test-hide" }
]
}
]
dynamicRight: [
{
type: "creator-panel-dynamic"
hidden: { archived: true }
additionalParams: { noDrag: "true", disableCollapse: "true" }
}
]
}
}
Main menu configuration
Tabs (in the side menu, such as Scenarios etc.) can be configured in the following way:
tabs: ${tabs} [
{
id: "kibana",
title: "Kibana",
url: "https://myKibana.org/kibana",
type: "IFrame",
requiredPermission: "AdminTab"
}
]
By default, the Scenarios and Components tabs and a Docs link (to https://docs.nussknacker.io/) are configured.
| Parameter name | Type | Default value | Description |
|---|---|---|---|
| id | string | Unique identifier | |
| title | string | Title appearing in UI | |
| type | IFrame/Local/Remote/Url | Type of tab (see below for explanation) | |
| url | string | URL of the tab | |
| requiredPermission | string | Optional parameter, name of Global Permission | |
| adminOnly | boolean | false | Optional. When true, the tab is shown to admins only - other users neither see it in the menu nor can open its url |
| configurationRecovery | boolean | false | Optional. When true, tab is available also in CONFIGURATION_ERROR mode (for recovery/fix flows) |
| accessTokenInQuery.enabled | boolean | false | When true the parameter holding access token (if OAuth2 authentication is used) will be added to iframe query parameters |
| accessTokenInQuery.parameterName | string | auth_token | Optional name of query parameter that holds the access token |
| embedTokenInQuery | boolean | false | When true a user signed in with a password gets a single-use nuEmbedToken in iframe query parameters, see Signing in to the dashboard |
The types of tabs can be as follows (see dev-application.conf for some examples):
- IFrame - contents of the url parameter will be embedded as IFrame
- Local - redirect to Designer page (
/scenarios,/componentsetc., seedesigner/client/packages/shell/src/containers/RootRoutes.tsxfor other options) - Remote - module federation can be used to embed external tabs, url should be in form:
{module}/{path}@{host}/{remoteEntry}.js - Url - redirect to external page/url
Sticky notes configuration
Sticky notes on the scenario graph are configured in the stickyNotesSettings section:
stickyNotesSettings: {
maxContentLength: 5000
maxNotesCount: 20
enabled: true
}
| Parameter name | Type | Default value | Description |
|---|---|---|---|
| maxContentLength | int | 5000 | Maximum length of a sticky note's content (characters) |
| maxNotesCount | int | Optional maximum number of sticky notes in one scenario or fragment. When not set, the number is not limited | |
| enabled | boolean | true | When false, the sticky notes feature is disabled: sticky notes can't be created and are not loaded into the graph |
Topics extension
The enterprise Topics extension adds the Topics page, where users create, edit, preview and
remove Kafka topics and their schemas. It is configured under topicsExtension and shown through a Remote tab
({id: "topics", url: "nuEnterpriseTopics/tab@/static/topics/remoteEntry.js", title: "Topics", type: "Remote"}).
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| topicsExtension.processingType | High | string | Processing type whose model configuration supplies the Kafka and schema registry connection (components.webhook.config) and the namespace of topic names | |
| topicsExtension.topicLimit | Medium | int | no limit | Maximum number of topics of this instance; adding a topic beyond it is refused |
| topicsExtension.defaultPartitions | Medium | int | Number of partitions of a newly created topic | |
| topicsExtension.defaultReplicationFactor | Medium | short | Replication factor of a newly created topic | |
| topicsExtension.defaultRetentionHours | Medium | int | Retention of a new topic when none is given, e.g. when it is created with the Create New Topic button of a Kafka source or sink | |
| topicsExtension.maxMessageBytes | Medium | long | max.message.bytes of a newly created topic |
All parameters except topicLimit are required.
Access control (RBAC) extension
The enterprise rbac extension keeps accounts, roles, groups and role bindings in the Designer's PostgreSQL
database and manages them in the Access control panel (Roles, Principals, Access review, Audit log), under
whichever authentication method is configured. It replaces the users/rules lists of the authentication
configuration, which it migrates into the database on first start and never reads again - see the
Migration Guide.
To use it:
- Put the extension on the Designer's classpath. The
nussknackerDesigner image contains it in/opt/nussknacker/extensions/rbac/, so add/opt/nussknacker/extensions/rbac/*toCLASSPATH. - Add its tab, which the Access control menu entries are shown under:
{id: "rbac", url: "nuEnterpriseRbac/tab@/static/rbac/remoteEntry.js", title: "Access control", type: "Remote"}. - Use PostgreSQL as the Designer's database. With any other database the Designer does not start.
- Switch on what the panel may create, since by default it creates neither local accounts nor groups: with Basic
Auth
rbac.principals.localUserCreationEnabled: true, with an identity providerrbac.principals.groupCreationEnabled: trueto match its groups or machine clients. Accounts and groups the migration created work either way, and service accounts can always be created. - Make sure somebody holds
System Adminafter the migration - an account or anisAdminrule in theusers/ruleslists - or setrbac.bootstrapAdmin.enabled: truewithrbac.bootstrapAdmin.login. Otherwise the Designer does not start. - Change the passwords of the migrated accounts after signing in for the first time: the default
conf/users.confholdsadmin/adminand the like, and the migrated passwords keep working once the file is gone.
Every Designer instance re-reads the stored roles, bindings and local credentials every rbac.refreshInterval, so a
change made on one instance reaches the others with up to that delay. The instance that made it applies it at once.
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| rbac.bootstrapAdmin.enabled | optional | boolean | false | Creates a local account holding System Admin at startup, named by rbac.bootstrapAdmin.login, when no account has that login. An existing account is never changed. Off, a start where System Admin was never granted fails; once it was, a start where its grants have expired or their holders were deactivated only logs an error. |
| rbac.bootstrapAdmin.login | required with enabled | string | The login of the account to create. To get back in after every administrator was cut off, name a login that does not exist yet; afterwards deactivate that account and remove the password from the configuration. With Oidc or OAuth2 the account signs in with a password only where authentication.enableBasicAuthAccounts is on. | |
| rbac.bootstrapAdmin.password | optional | string | The password of the account to create. Without it, the account is created only when no principal holds System Admin, with a generated password written to rbac.bootstrapAdmin.credentialFile. | |
| rbac.bootstrapAdmin.credentialFile | optional | path | rbac-bootstrap-admin-credential | Where the generated password is written, relative to the working directory. Delete the file once the password is kept somewhere safer. |
| rbac.principals.localUserCreationEnabled | optional | boolean | false | Whether the panel creates local accounts. Off, accounts come from the identity provider and the users migration only. Switch it on with Basic Auth. |
| rbac.principals.groupCreationEnabled | optional | boolean | false | Whether the panel creates and edits groups and machine clients. Off, only those the rules migration created exist, and they can still be deactivated. |
| rbac.principals.groupsEnabled | optional | boolean | true | Whether groups matching claim values admit anyone. Existing ones, migrated too, admit and grant nobody while this is off. |
| rbac.principals.machineClientsEnabled | optional | boolean | true | Whether groups recognising a machine client by its subject admit anyone. Existing ones, migrated too, admit and grant nobody while this is off. |
| rbac.grants.requireExpiry | optional | boolean | false | Refuses a role grant without an expiry, and the panel stops offering one. Tokens then expire too: rbac.apiTokens.maxLifetime cannot be unlimited, or the Designer does not start. |
| rbac.grants.defaultExpiry | optional | duration | 90d | What a new grant's expiry is set to in the panel before anyone picks one. At least 1d. |
| rbac.refreshInterval | optional | duration | 10s | How often each instance re-reads roles, bindings and local credentials. The longest a change made on another instance takes to apply here, and the longest a deactivated account or a reset password still signs in. |
Environment configuration
Nussknacker installation may consist of more than one environment. Typical example is a configuration, where you have:
- environment for testing scenarios, which mirrors production data (e.g. via Kafka mirror-maker), but contains only scenarios that are currently worked on
- production environment
You can configure secondaryEnvironment to allow for
- easy migration of scenarios
- comparing scenarios between environments
- testing (currently only via REST API) if all scenarios from secondary environment are valid with model configuration from this environment (useful for testing configuration etc.) Currently, you can only configure secondary environment if it uses BASIC authentication - technical user is needed to access REST API.
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| environment | Medium | string | Used mainly for metrics configuration. Please note: it has to be consistent with the env tag of the metrics - in the Docker Compose installation set by telegraf/telegraf.conf from NU_ENVIRONMENT | |
| environmentAlert.content | Low | string | Human readable name of environment, to display in UI | |
| environmentAlert.color | Low | indicator-green / indicator-blue / indicator-yellow / indicator-red | Color of environment indicator | |
| secondaryEnvironment.uri | Medium | string | URL of Nussknacker REST API e.g. http://secondary.host:8080/api | |
| secondaryEnvironment.user | Medium | string | User that should be used for migration/comparison | |
| secondaryEnvironment.password | Medium | string | Password of the user that should be used for migration/comparison | |
| secondaryEnvironment.remoteConfig.batchSize | Low | int | 10 | For testing compatibility we have to load all scenarios, we do it in batches to optimize |
| secondaryEnvironment.targetEnvironmentId | Low | string | Name of the secondary environment (used mainly for messages for user) |
Testing
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| testDataSettings.maxSamplesCount | Medium | int | 20 | Limits the number of samples to be generated and received from a file |
| testDataSettings.testDataMaxLength | Low | int | 200000 | Limits the size (in characters) of the test data generated and received in tests from a file |
| testDataSettings.resultsMaxBytes | Low | long | 50000000 | Limits the size (in bytes) of returned test results (i.e. variables for each node, invocation results and errors) |
| testCasesSettings.multipleEnabled | Low | boolean | true | Allows more than one test case per scenario |
| testCasesSettings.multipleParallelismExecution | Low | int | 1 | How many test cases are run at the same time by Run all |
Monitoring metrics
The admin-only Monitoring > Metrics page reads historical series from InfluxDB 1.x and links to the
corresponding Grafana dashboards. The Docker Compose installation configures it automatically. Standalone
installations can use:
monitoring.metrics {
influxUrl: "http://influxdb:8086/query"
database: "nussknacker_metrics"
environment: "local"
grafanaUrl: "/grafana"
# user: ${INFLUXDB_USER}
# password: ${INFLUXDB_PASSWORD}
timeout: 10 seconds
}
| Parameter | Default | Meaning |
|---|---|---|
monitoring.metrics.influxUrl | absent | Full HTTP(S) InfluxDB endpoint ending in /query, without credentials in the URL. When absent, the page reports that metrics storage is not configured. |
monitoring.metrics.database | nussknacker_metrics | The same database as the Grafana influx datasource. |
monitoring.metrics.environment | local | Exact env tag to read; also passed to Grafana links. |
monitoring.metrics.user, password | absent | Optional InfluxDB Basic authentication, used only by the Designer backend. |
monitoring.metrics.grafanaUrl | absent | Grafana base URL, absolute or relative. Links are omitted when absent. |
monitoring.metrics.flinkReporter | influxdb | Flink reporter format: influxdb or prometheus for Telegraf inputs.prometheus with metric_version = 1. Version 2 is unsupported. Set this in HOCON; the distribution has no environment variable for it. |
monitoring.metrics.timeout | 10 seconds | Time budget for loading the overview, including requests waiting for a concurrency slot. |
The distribution's application.conf and dev-application.conf accept MONITORING_INFLUX_URL for the query
endpoint and the existing INFLUXDB_DATABASE, INFLUXDB_USER, INFLUXDB_PASSWORD variables. Environment and
Grafana URL follow the installation settings. Standalone Docker containers and the distribution launcher require
an explicit MONITORING_INFLUX_URL ending in /query; setting only INFLUXDB_URL does not enable historical
metrics. The shipped Docker Compose bundle supplies monitoring.metrics.influxUrl in its configuration, so it
does not require this variable.
The IDE launch configurations use the development InfluxDB port. This does not
change countsSettings or the scenario Metrics iframe.
Each series is queried independently so a failed query does not discard completed panels. The Designer allows
four active InfluxDB HTTP calls and four waiting calls across the shared metrics executor. Excess requests
receive a busy status; the Metrics page offers a manual Retry action and refreshes its time period normally
every 60 seconds. An overview uses at most four workers and stops scheduling remaining series when capacity is
exhausted. Browser cancellation does not cancel server work or an executing InfluxDB query: obsolete work is
bounded by these limits and the overview deadline.
Waiting work is removed at the deadline; executing HTTP calls retain their slots until they actually complete.
The overview supports scenario and time filters (up to 30 days). It uses the same queries and bucket duration as the generated overview panels in the bundled Grafana dashboards. Errors are rates per second; missing samples remain gaps. Designer and Assistant plots are installation-wide. The common Lite runtime source/error/service series remain available only in Grafana and are not queried by the Designer overview. Request-Response invocation series remain in the overview. Target filtering requires a target tag which current runtime metrics do not consistently export.
Import the bundled Grafana JSON dashboards and configure a datasource named influx before enabling their links.
The query catalog and generation/test instructions are in examples/installation/grafana/README.md.
Scenario metrics
The Metrics button of a scenario (toolbar button type scenario-metrics) opens the scenario metrics screen: the
most needed series of the scenario from the same InfluxDB and with the same queries as the bundled Grafana
dashboards, with links to Grafana for everything else. It needs monitoring.metrics configured as described in
Monitoring metrics; without it the screen says so.
scenarioMetrics {
# true: the screen opens over the scenario editor, keeping unsaved changes;
# false (default): it opens in place of the editor, which asks to save or discard unsaved changes first
openAsOverlay: false
}
| Parameter | Default | Meaning |
|---|---|---|
scenarioMetrics.openAsOverlay | false | How the Metrics button opens the screen. Also SCENARIO_METRICS_OPEN_AS_OVERLAY. |
Each user can override it in the Designer's Settings, under Scenario > Open metrics as overlay; Default keeps the configured value.
What it shows depends on the scenario:
- streaming on Flink: tiles for throughput, errors, latest event, Kafka lag, uptime, restarts and the last checkpoint; source throughput, events passed and rejected, errors; delay since event occurrence (median, p99), time from latest event, Kafka lag; median response time and failed calls per external service;
- streaming on Lite: the same without the Flink job tiles (uptime, restarts, last checkpoint);
- request-response: tiles for requests, outputs, errors and the peak 2xx and 4xx/5xx latency p99; responses by outcome, outputs, errors; latency median, p99 and max of successful and failed responses; external services as above.
Batch scenarios and fragments keep the Grafana dashboard at /metrics/<scenario>.
The screen is served by GET /api/monitoring/scenarios/{scenarioName}/metrics?period=15m|30m|1h|24h to anyone
holding Scenario.View on the scenario. Its cost is bounded:
- four fixed periods, the longest a day, about 90-180 points per series;
- 13 InfluxDB statements per screen for Flink, 9 for Lite, 8 for request-response (the Monitoring > Metrics
overview runs about 38), each its own call through the executor shared with the overview: four active and four
waiting calls, then
busy; - the period ends at the start of the current step (10 s, 10 s, 20 s, 8 min) and the answer is cached for that step, keyed by scenario, period and end, so everyone watching one scenario shares one set of calls per step; a busy or failed answer is not cached;
- the screen refreshes once a minute, only while it is visible, never retries a busy answer by itself (Retry does) and cancels its request when closed.
The queries are in scenario-metrics-catalog.json, next to the overview's metrics-catalog.json, with the same
monitoring.metrics.flinkReporter handling.
Other configuration options
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| attachments.maxSizeInBytes | Medium | long | 10485760 | Limits max size of scenario attachment, by default to 10mb |
| analytics.engine | Low | Matomo | Currently only available analytics engine is Matomo | |
| analytics.url | Low | string | URL of Matomo server | |
| analytics.siteId | Low | string | Site id | |
| intervalTimeSettings.processes | Low | int | 20000 | How often frontend reloads scenario list |
| intervalTimeSettings.healthCheck | Low | int | 30000 | How often frontend reloads checks scenarios states |
| developmentMode | Medium | boolean | false | For development mode we disable some security features like CORS. Don't use in production |
| enableConfigEndpoint | Medium | boolean | false | Expose config over http (GET /api/app/config/) - requires admin permission. Please mind, that config often contains password or other confidential data - this feature is meant to be used only on 'non-prod' envrionments. |
| redirectAfterArchive | Low | boolean | true | Redirect to scenarios list after archive operation. |
| scenarioLabelSettings.validationRules | Low | array | [] | Allows to configure validation rules for scenario labels |
| scenarioLabelSettings.validationRules[0].validationPattern | Low | string | Regular expression for scenario label validation | |
| scenarioLabelSettings.validationRules[0].validationMessage | Low | string | Hint shown to the user when the scenario label does not match the given validation regex | |
| scenarioStateTimeout | Low | duration | 5 seconds | Timeout for fetching scenario state operation |
| monitoring.scenarioViewTracking.enabled | Low | boolean | true | Whether the time users spend in the Designer, and with each scenario presented, is recorded. It feeds the person timeline on Monitoring / Activity, Online now on Status and presence summaries on Monitoring / Activity, so turning it off empties all of them. Sessions recorded before it was turned off are still removed after retention |
| monitoring.scenarioViewTracking.inactivityGap | Low | duration | 3 minutes | How long a break in looking at a scenario still counts as the same session. There is no event saying that somebody closed a scenario, so this is what decides where one session ends and the next begins. Has to stay well above the interval the UI reports with - browsers throttle a background tab to about a report a minute - so that a hidden window does not cut one sitting into pieces |
| monitoring.scenarioViewTracking.minimumTimeBetweenReports | Low | duration | 15 seconds | How rarely a report is written down, however often it arrives, per thing being reported - a scenario, or simply having the Designer open. The first switch to another version of a scenario within that time is written down at once. It decides the write load of the feature, and also how much of a session is lost at its end, which is never reported. Has to stay at or below half of onlineWindow, and well below inactivityGap |
| monitoring.scenarioViewTracking.onlineWindow | Low | duration | 45 seconds | How long after their last report a user is still shown as looking at what they last reported, on the Status page. Has to stay at or above twice minimumTimeBetweenReports, and below inactivityGap. A report arriving just before the throttle lets one through waits out another whole interval, so a viewer who never left can go unrecorded for the throttle plus the interval the UI reports with, and would blink out of the page if the window did not cover it |
| monitoring.scenarioViewTracking.retention | Low | duration | 92 days | How long a recorded session is kept. Nothing else removes them, and the person timeline on Monitoring / Activity reaches back monitoring.userActivities.maxTimelinePeriod at most |
| monitoring.scenarioViewTracking.cleanupInterval | Low | duration | 1 hour | How often the sessions older than the retention are swept away. Runs on the leader only |
| monitoring.scenarioViewTracking.cleanupTimeout | Low | duration | 1 minute | How long one sweep is waited for before it is abandoned and retried next time round |
| monitoring.engineDiagnostics.cacheTTL | Low | duration | 30 seconds | How long the answer of the Engines section is served before the engines are asked again. Shared between everybody looking, so this caps how much traffic the section can cause |
| monitoring.engineDiagnostics.timeout | Low | duration | 10 seconds | How long a single engine has to answer before it is reported as not answering |
| monitoring.insights.hourlyBucketsUpTo | Low | duration | 48 hours | Up to this long a period is drawn in hourly buckets in the summaries on Monitoring / Activity, above it in daily ones. At most 14 days |
| monitoring.insights.liveDataFreshness | Low | duration | 5 minutes | How long after its last report a scenario still counts as reporting live data rather than being stale |
| monitoring.insights.liveDataHorizon | Low | duration | 24 hours | How far back a live data row is still worth reading. Past this it describes a deployment that is long gone |
| monitoring.insights.liveDataCollectorSilence | Low | duration | 15 seconds | How long one collector of a scenario may fall behind the scenario's most recent upload before what it reported is no longer added up: it is taken for one that a restart replaced. Keep it well above how often the engines upload (uploadIntervalInSeconds, and idleIntervalInSeconds when samples are uploaded on demand) |
| monitoring.userActivities.defaultLimit | Low | int | 100 | How many activities the user activity API answers with when no limit is given |
| monitoring.userActivities.maxLimit | Low | int | 1000 | The most activities the user activity API answers with. A larger limit is lowered to this; hasMore tells there are more |
| monitoring.userActivities.usersCacheTTL | Low | duration | 5 minutes | How long the list of users the pickers offer is served before it is read again. Counting the activities of every user reads the whole activity table, so the answer is shared between everybody looking |
| monitoring.userActivities.defaultTimelinePeriod | Low | duration | 7 days | The period the person timeline on Monitoring / Activity covers when none is asked for |
| monitoring.userActivities.maxTimelinePeriod | Low | duration | 92 days | The longest period the person timeline on Monitoring / Activity can be asked for. Does not limit the paginated audit table |
| usageStatisticsReports.enabled | Low | boolean | true | When enabled browser will send anonymous usage statistics reports to stats.nussknacker.io |
| usageStatisticsReports.errorReportsEnabled | Low | boolean | true | When enabled browser will send anonymous errors reports to stats.nussknacker.io |
| assistantSettings.enabled | Low | boolean | false | Whether the AI assistant is shown in the Designer. The assistant itself is configured in assistantHttpService |
AI assistant
The AI assistant consists of the chat in the Designer and the assistant extension running in the Designer's backend. To use it:
- Put the extension on the Designer's classpath. The
nussknackerDesigner image contains it in/opt/nussknacker/extensions/assistant/, so add/opt/nussknacker/extensions/assistant/*toCLASSPATH. - Turn the chat on with
assistantSettings.enabled: true. - Configure the assistant under
assistantHttpService.assistant. Without this section the extension starts thedummyassistant, which only echoes the question back - useful for checking the setup, not for users. An installation that ships with the chat on but without a model setstype: "not-configured", so that users are told the assistant needs configuring instead of getting an echo.
The LLM assistant answers questions from documentation embeddings: a file, built from this documentation, that is searched for the fragments relevant to a question. The model itself is called through the API of the configured provider, with your API key.
assistantSettings {
enabled: true
}
assistantHttpService {
assistant {
type: "llm"
llmModel {
type: "anthropic"
apiKey: ${ASSISTANT_API_KEY}
modelName: "claude-sonnet-4-6"
# tool calls that build scenarios carry large arguments; a low output limit cuts them off
maxTokens: 16000
}
embeddingsResourceUri: "file:/opt/nussknacker/designer-assistant-embeddings/embeddings.store"
# lets the model read and edit scenarios with the Designer's tools
useExternalTools: true
historyMaxMessages: 200
}
}
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| assistantHttpService.assistant.type | High | string | dummy | llm for the assistant backed by a language model, not-configured for one that answers every question that no model is configured and the administrator has to set it up, dummy for a stub that echoes the question back |
| assistantHttpService.assistant.llmModel | High | object | The model to use, see the table below. Required for llm | |
| assistantHttpService.assistant.embeddingsResourceUri | High | URI | Location of the documentation embeddings file, as a file: or classpath: URI. Required for llm | |
| assistantHttpService.assistant.useExternalTools | High | boolean | false | Whether the model may call the tools the Designer sends with each message, i.e. read and edit scenarios, manage topics, read skills. When false the assistant only answers questions from the documentation |
| assistantHttpService.assistant.prompt | Low | string | built-in prompt | System prompt of the assistant. Replacing it replaces the built-in instructions entirely |
| assistantHttpService.assistant.historyMaxMessages | Medium | int | 50 | How many of the latest messages of a conversation the assistant remembers. Every tool call takes several messages, so building a scenario with tools needs a larger value, e.g. 200 |
| assistantHttpService.assistant.contentRetrieverMaxResults | Low | int | 15 | How many documentation fragments are retrieved for one search |
| assistantHttpService.assistant.contentRetrieverMinScore | Low | double | Minimal relevance score (between 0 and 1) of a documentation fragment to be used. No limit when not set |
llmModel.type selects the provider. Each provider has its own fields:
llmModel.type | Fields (defaults) |
|---|---|
open-ai | apiKey, modelName (gpt-4.1-mini), url, temperature, topP, maxCompletionTokens, maxRetries (3) |
mistral | apiKey, modelName (mistral-medium-latest), temperature, topP, maxTokens, maxRetries (3) |
anthropic | apiKey, modelName (claude-haiku-4-5), temperature, topP, maxTokens, maxRetries (3) |
one-of | llmModels - a list of the models above, tried in order: when one fails, the next one is used |
apiKey is required; the other fields are optional. maxRetries applies only to non-streaming requests.
With url the open-ai provider talks to any OpenAI-compatible server (for example LM Studio, Ollama or vLLM) instead of the OpenAI API, e.g. url: "http://localhost:1234/v1". Such servers usually ignore the key, but it must still be set.
Conversations are kept in the Designer's memory and are lost when the Designer restarts.
Usage statistics storage (QuestDB)
The Designer keeps the usage statistics sent by the browser (POST /api/statistic) in an embedded
QuestDB database stored on the local disk. They are summed up per day, and today's sums are
added to the anonymous usage statistics report (see usageStatisticsReports above). Nothing else is stored there, and
QuestDB failing to start only turns the collection off - the Designer starts anyway. The storage is on unless
questDbSettings.enabled is set to false.
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| questDbSettings.enabled | Low | boolean | true | Whether the statistics sent by the browser are collected |
| questDbSettings.directory | Low | string | <java.io.tmpdir>/nu/<instanceId> | Directory of the QuestDB files |
| questDbSettings.instanceId | Low | string | designer-statistics | Name of the directory under the temporary directory when directory is not set. With several Designer instances on one host, give each its own |
| questDbSettings.tasksExecutionDelay | Low | duration | 30 seconds | How often the statistics gathered in memory are written to QuestDB |
| questDbSettings.retentionDelay | Low | duration | 24 hours | How often the days before today are dropped |
| questDbSettings.poolConfig.corePoolSize | Low | int | 2 | Core size of the thread pool reading and writing the statistics. When poolConfig is set, all four of its fields have to be given |
| questDbSettings.poolConfig.maxPoolSize | Low | int | 4 | Maximum size of that thread pool |
| questDbSettings.poolConfig.keepAliveTimeInSeconds | Low | long | 60 | How long an idle thread above the core size is kept |
| questDbSettings.poolConfig.queueCapacity | Low | int | 8 | Capacity of the queue of that thread pool |
Instance logs extension
The instance-logs Designer extension (extensions/instance-logs/instance-logs-extension.jar in the Designer image,
activated by adding /opt/nussknacker/extensions/instance-logs/* to the CLASSPATH) serves the Logs toolbar,
shown on every page, by default at the bottom. Its entries explain a failed deployment or an integration that does
not work without a log stack of your own:
- The Designer's own entries, kept in memory from its start: deployments, validation, integrations used while designing a scenario, and the scenarios running in the Designer's JVM (embedded request-response, a Flink MiniCluster). The logging configuration does not change, and the entries do not survive a restart.
- The entries of the Flink clusters the
flinkStreamingscenario types deploy to, the job manager's and the task managers'. WithinstanceLogs.flink.push.tokenset, the Designer listens oninstanceLogs.flink.push.portand the Flink processes send it their entries, with the MDC (scenario, node, event code) and stack traces, as they log them - see Sending the Flink logs. Like the Designer's own, they are kept in memory and do not survive the Designer's restart. Without a token, the Designer reads the processes' log files over the cluster's REST API instead; Flink serves a file only whole, so each read downloads every file that has grown. Flink's default file layout does not print the MDC either: add%Xbefore the-ofappender.rolling.layout.patternin Flink'slog4j-console.propertiesto keep it.
The toolbar's Instance tab shows the default filters; changing a filter on it opens a tab of its own, and a scenario gets its tab the first time the logs are seen while it is open. The tabs are kept in the browser. When the logs cannot be read, without the extension or with it failing, the toolbar points to this section.
Logs.ViewInstance (global) shows every entry, Logs.ViewScenario (per category, under RBAC) the entries of the
scenarios in a category. Under the users/rules configuration only an admin, or a role with a global grant, holds
them.
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| instanceLogs.defaultFilters.level | Medium | string | INFO | The lowest level the logs start from: TRACE, DEBUG, INFO, WARN or ERROR |
| instanceLogs.defaultFilters.sources | Low | list | all | The sources the logs start from: designer, jobmanager, taskmanager |
| instanceLogs.defaultFilters.eventsOnly | Low | boolean | false | Start from the curated events of the nussknacker.events logger only |
| instanceLogs.refreshInterval | Low | duration | 3 seconds | How often the toolbar asks for new entries while following them |
| instanceLogs.buffer.level | Medium | string | INFO | The lowest level of the Designer's entries kept; it changes nothing the loggers emit |
| instanceLogs.buffer.maxEvents | Low | int | 10000 | How many of the Designer's entries are kept |
| instanceLogs.buffer.maxChars | Low | long | 16777216 | How much text all the kept entries may take together; the oldest go first |
| instanceLogs.flink.enabled | Low | boolean | true | Read the Flink clusters' log files |
| instanceLogs.flink.restUrls | Low | list | The Flink clusters to read; by default those of the flinkStreaming scenario types not deploying to a MiniCluster | |
| instanceLogs.flink.maxTailBytes | Low | long | 4194304 | How much of the end of each log file is read |
| instanceLogs.flink.refreshInterval | Low | duration | 5 seconds | How often the log files are read at most; a file is read again only once its size changes |
| instanceLogs.flink.timeZone | Low | string | UTC | The zone of the timestamps in the log files |
| instanceLogs.flink.push.token | Medium | string | Turns on receiving the entries the Flink processes send, and only those carrying this token are taken | |
| instanceLogs.flink.push.host | Low | string | 0.0.0.0 | The address listened on for them |
| instanceLogs.flink.push.port | Low | int | 4560 | The port listened on for them |
| instanceLogs.flink.push.maxConnections | Low | int | 256 | How many connections are kept open at once, one per Flink process; those above are refused |
| instanceLogs.flink.push.maxPendingConnections | Low | int | 16 | How many of them may have sent no entry with the token yet; above it the oldest is closed |
Sending the Flink logs
The Flink processes send their entries with Log4j's own Socket appender and JsonTemplateLayout, both in Flink's
lib, behind an Async appender that drops entries rather than hold up Flink's threads once the Designer cannot keep
up or is gone. Add this to log4j-console.properties of the job manager and the task managers, with
INSTANCE_LOGS_TOKEN in their environment set to the Designer's instanceLogs.flink.push.token, and
INSTANCE_LOGS_HOST / INSTANCE_LOGS_PORT to where the Designer listens:
appender.instanceLogs.name = InstanceLogsSocket
appender.instanceLogs.type = Socket
appender.instanceLogs.host = ${env:INSTANCE_LOGS_HOST:-designer}
appender.instanceLogs.port = ${env:INSTANCE_LOGS_PORT:-4560}
appender.instanceLogs.protocol = TCP
appender.instanceLogs.immediateFail = false
appender.instanceLogs.reconnectionDelayMillis = 10000
appender.instanceLogs.connectTimeoutMillis = 3000
appender.instanceLogs.ignoreExceptions = true
appender.instanceLogs.layout.type = JsonTemplateLayout
appender.instanceLogs.layout.eventDelimiter = \n
appender.instanceLogs.layout.eventTemplate = {"time": {"$resolver": "timestamp", "epoch": {"unit": "millis", "rounded": true}}, "level": {"$resolver": "level", "field": "name"}, "logger": {"$resolver": "logger", "field": "name"}, "thread": {"$resolver": "thread", "field": "name"}, "message": {"$resolver": "message", "stringified": true}, "exception": {"$resolver": "exception", "field": "stackTrace", "stackTrace": {"stringified": true}}, "mdc": {"$resolver": "mdc"}, "source": "${hostName}", "logFile": "${sys:log.file:-}", "token": "${env:INSTANCE_LOGS_TOKEN:-}"}
appender.instanceLogsAsync.name = InstanceLogsAsync
appender.instanceLogsAsync.type = Async
appender.instanceLogsAsync.blocking = false
appender.instanceLogsAsync.bufferSize = 8192
appender.instanceLogsAsync.appenderRef.type = AppenderRef
appender.instanceLogsAsync.appenderRef.ref = InstanceLogsSocket
rootLogger.appenderRef.instanceLogs.ref = InstanceLogsAsync
The token travels in plain text with every entry: keep the port reachable from the Flink processes only. The
examples/installation bundle sets all of this up for its Flink cluster profile.
Node actions extension
The nodes-actions Designer extension (extensions/nodes-actions/nodes-actions.jar in the Designer image, activated
by adding it to the CLASSPATH) serves the action buttons of the node window: it creates the topic behind an HTTP
endpoint (generate-endpoint), sends the data sample of a source to that topic (send-sample-request) and checks a
WebSocket subscription (test-ws-forwarder). It is exposed under /api/custom/nodes, e.g.
POST /api/custom/nodes/{scenarioName}/actions with the action name in actionName.
Messages are sent to a topic, and WebSocket subscriptions are checked, through
Blabber, the HTTP and WebSocket gateway to Kafka. When the extension
is on the classpath, nodesActionsHttpService is required: without gateway.httpApiGatewayFormatUrl and
blabberManagement the Designer does not start.
| Parameter name | Importance | Type | Default value | Description |
|---|---|---|---|---|
| nodesActionsHttpService.gateway.httpApiGatewayFormatUrl | High | string | URL a message for a topic is POSTed to, with $topicName where the topic name goes | |
| nodesActionsHttpService.gateway.auth.username | Medium | string | Basic Auth user for the gateway. Without auth, the requests carry no credentials; with it, both fields have to be non-empty | |
| nodesActionsHttpService.gateway.auth.password | Medium | string | Basic Auth password for the gateway | |
| nodesActionsHttpService.blabberManagement.baseUri | High | string | Base URI of the Blabber management API; WebSocket subscriptions are checked with POST <baseUri>/forwarding/test | |
| nodesActionsHttpService.blabberManagement.accessToken | High | string | Bearer token for the Blabber management API |
nodesActionsHttpService {
gateway {
httpApiGatewayFormatUrl: "http://blabber:8147/topics/$topicName"
auth {
username: ${BLABBER_PUBLISHER_USER}
password: ${BLABBER_PUBLISHER_PASSWORD}
}
}
blabberManagement {
baseUri: "http://blabber:8147/management"
accessToken: ${BLABBER_ACCESS_TOKEN}
}
}
The topics themselves are created through the topics registry, configured separately.
Scenario type, categories
Every scenario has to belong to a group called category. Category defines the business area around which you can organize
users permissions.
For example, in one Nussknacker installation you can have scenarios detecting frauds, and those implementing marketing campaigns. Then, the configuration will look like:
scenarioTypes {
streaming-marketing {
deploymentConfig {
(...)
}
modelConfig {
(...)
}
category: "Marketing"
}
streaming-fraud-detection {
deploymentConfig {
(...)
}
modelConfig {
(...)
}
category: "Fraud Detection"
}
}
Scenario type configuration consists of parts:
deploymentConfig- scenario deployment configurationmodelConfig- model configurationcategory- category handled by given scenario type
In Nussknacker distribution there are preconfigured scenario types:
streaming- using Flink Deployment Manager providing both stateful and stateless streaming componentsstreaming-lite-embedded- using embedded Lite Deployment Manager in Streaming processing mode providing only stateless streaming componentsrequest-response-embedded- use embedded Request-Response Deployment Manager, scenario logic is exposed as REST API, on additional HTTP port at Designer
Each of these scenario types has Default category assigned.