Dela via


Filtrera och förbearbeta telemetri i Application Insights SDK

Du kan skriva kod för att filtrera, ändra eller utöka telemetrin innan den skickas från SDK:t. Bearbetningen innehåller data som skickas från standardtelemetrimodulerna, till exempel INSAMLING av HTTP-begäranden och beroendeinsamling.

  • Filtrering kan ändra eller ta bort telemetri innan den skickas från SDK:t genom att implementera ITelemetryProcessor. Du kan till exempel minska mängden telemetri genom att exkludera begäranden från robotar. Till skillnad från sampling har du fullständig kontroll över vad som skickas eller tas bort, men det påverkar alla mått som baseras på aggregerade loggar. Beroende på hur du tar bort objekt kan du också förlora möjligheten att navigera mellan relaterade objekt.

  • Lägg till eller ändra egenskaper i telemetri som skickas från din app genom att implementera en ITelemetryInitializer. Du kan till exempel lägga till beräknade värden eller versionsnummer för att filtrera data i portalen.

  • Sampling minskar mängden telemetri utan att påverka din statistik. Den håller ihop relaterade datapunkter så att du kan navigera mellan dem när du diagnostiserar ett problem. I portalen multipliceras det totala antalet för att kompensera för samplingen.

Kommentar

SDK-API:et används för att skicka anpassade händelser och mått.

Förutsättningar

Installera lämplig SDK för ditt program: ASP.NET, ASP.NET Core, Icke-HTTP/Worker för .NET/.NET Core eller JavaScript.

Filtrering

Den här tekniken ger dig direkt kontroll över vad som ingår eller undantas från telemetriströmmen. Filtrering kan användas för att ta bort telemetriobjekt från att skickas till Application Insights. Du kan använda filtrering med sampling eller separat.

Om du vill filtrera telemetri skriver du en telemetriprocessor och registrerar den med TelemetryConfiguration. All telemetri går igenom processorn. Du kan välja att släppa den från strömmen eller ge den till nästa processor i kedjan. Telemetri från standardmodulerna, till exempel HTTP-begärandeinsamlaren och beroendeinsamlaren, och telemetri som du spårade själv ingår. Du kan till exempel filtrera bort telemetri om begäranden från robotar eller lyckade beroendeanrop.

Varning

Om du filtrerar telemetrin som skickas från SDK:t med hjälp av processorer kan du förvränga den statistik som visas i portalen och göra det svårt att följa relaterade objekt.

Överväg i stället att använda sampling.

.NET-program

  1. Implementera ITelemetryProcessor.

    Telemetriprocessorer skapar en bearbetningskedja. När du instansierar en telemetriprocessor får du en referens till nästa processor i kedjan. När en telemetridatapunkt skickas till processmetoden utför den sitt arbete och anropar sedan (eller anropar inte) nästa telemetriprocessor i kedjan.

    using Microsoft.ApplicationInsights.Channel;
    using Microsoft.ApplicationInsights.Extensibility;
    using Microsoft.ApplicationInsights.DataContracts;
    
    public class SuccessfulDependencyFilter : ITelemetryProcessor
    {
        private ITelemetryProcessor Next { get; set; }
    
        // next will point to the next TelemetryProcessor in the chain.
        public SuccessfulDependencyFilter(ITelemetryProcessor next)
        {
            this.Next = next;
        }
    
        public void Process(ITelemetry item)
        {
            // To filter out an item, return without calling the next processor.
            if (!OKtoSend(item)) { return; }
    
            this.Next.Process(item);
        }
    
        // Example: replace with your own criteria.
        private bool OKtoSend (ITelemetry item)
        {
            var dependency = item as DependencyTelemetry;
            if (dependency == null) return true;
    
            return dependency.Success != true;
        }
    }
    
  2. Lägg till processorn.

    Infoga det här kodfragmentet i ApplicationInsights.config:

    <TelemetryProcessors>
      <Add Type="WebApplication9.SuccessfulDependencyFilter, WebApplication9">
        <!-- Set public property -->
        <MyParamFromConfigFile>2-beta</MyParamFromConfigFile>
      </Add>
    </TelemetryProcessors>
    

    Du kan skicka strängvärden från .config-filen genom att ange offentliga namngivna egenskaper i klassen.

    Varning

    Var noga med att matcha typnamnet och eventuella egenskapsnamn i .config-filen med klass- och egenskapsnamnen i koden. Om .config-filen refererar till en icke-existerande typ eller egenskap kan SDK:t i tysthet misslyckas med att skicka telemetri.

    Du kan också initiera filtret i koden. I en lämplig initieringsklass, till exempel AppStart i Global.asax.cs, infogar du processorn i kedjan:

    var builder = TelemetryConfiguration.Active.DefaultTelemetrySink.TelemetryProcessorChainBuilder;
    builder.Use((next) => new SuccessfulDependencyFilter(next));
    
    // If you have more processors:
    builder.Use((next) => new AnotherProcessor(next));
    
    builder.Build();
    

    Telemetriklienter som skapats efter den här punkten använder dina processorer.

Exempelfilter

Syntetiska begäranden

Filtrera bort robotar och webbtester. Även om Metrics Explorer ger dig möjlighet att filtrera bort syntetiska källor, minskar det här alternativet trafik- och inmatningsstorleken genom att filtrera dem i själva SDK:t.

public void Process(ITelemetry item)
{
    if (!string.IsNullOrEmpty(item.Context.Operation.SyntheticSource)) {return;}
    
    // Send everything else:
    this.Next.Process(item);
}

Autentiseringen misslyckades

Filtrera bort begäranden med svaret "401".

public void Process(ITelemetry item)
{
    var request = item as RequestTelemetry;

    if (request != null &&
    request.ResponseCode.Equals("401", StringComparison.OrdinalIgnoreCase))
    {
        // To filter out an item, return without calling the next processor.
        return;
    }

    // Send everything else
    this.Next.Process(item);
}

Filtrera bort snabba fjärrberoendeanrop

Om du bara vill diagnostisera anrop som är långsamma filtrerar du bort de snabba.

Kommentar

Den här filtreringen förvränger den statistik som visas på portalen.

public void Process(ITelemetry item)
{
    var request = item as DependencyTelemetry;

    if (request != null && request.Duration.TotalMilliseconds < 100)
    {
        return;
    }
    this.Next.Process(item);
}

Diagnostisera beroendeproblem

Den här bloggen beskriver ett projekt för att diagnostisera beroendeproblem genom att automatiskt skicka regelbundna pingar till beroenden.

Java-program

Mer information om telemetriprocessorer och deras implementering i Java finns i dokumentationen för Java-telemetriprocessorer.

JavaScript-webbprogram

Du kan filtrera telemetri från JavaScript-webbprogram med hjälp av ITelemetryInitializer.

  1. Skapa en telemetriinitieringsåteranropsfunktion. Återanropsfunktionen tar ITelemetryItem som en parameter, vilket är den händelse som bearbetas. Om du returnerar false från det här återanropet kommer telemetriobjektet att filtreras bort.

    var filteringFunction = (envelope) => {
      if (envelope.data.someField === 'tobefilteredout') {
        return false;
      }
      return true;
    };
    
  2. Lägg till telemetriinitierarens motringning:

    appInsights.addTelemetryInitializer(filteringFunction);
    

Lägg till/ändra egenskaper: ITelemetryInitializer

Använd telemetriinitierare för att utöka telemetri med ytterligare information eller för att åsidosätta telemetriegenskaper som anges av standardtelemetrimodulerna.

Application Insights för ett webbpaket samlar till exempel in telemetri om HTTP-begäranden. Som standard flaggas alla begäranden med svarskoden >=400 som misslyckade. Om du i stället vill behandla 400 som en lyckad initiering kan du ange en telemetriinitierare som anger den lyckade egenskapen.

Om du anger en telemetriinitierare anropas den när någon av Metoderna Track*() anropas. Den här initiatorn innehåller Track() metoder som anropas av standardtelemetrimodulerna. Enligt konventionen anger dessa moduler inte någon egenskap som redan har angetts av en initiator. Telemetriinitierare anropas innan telemetriprocessorer anropas, så alla berikanden som görs av initiatorer är synliga för processorer.

.NET-program

  1. Definiera initieraren

    using System;
    using Microsoft.ApplicationInsights.Channel;
    using Microsoft.ApplicationInsights.DataContracts;
    using Microsoft.ApplicationInsights.Extensibility;
    
    namespace MvcWebRole.Telemetry
    {
      /*
       * Custom TelemetryInitializer that overrides the default SDK
       * behavior of treating response codes >= 400 as failed requests
       *
       */
        public class MyTelemetryInitializer : ITelemetryInitializer
        {
            public void Initialize(ITelemetry telemetry)
            {
                var requestTelemetry = telemetry as RequestTelemetry;
                // Is this a TrackRequest() ?
                if (requestTelemetry == null) return;
                int code;
                bool parsed = Int32.TryParse(requestTelemetry.ResponseCode, out code);
                if (!parsed) return;
                if (code >= 400 && code < 500)
                {
                    // If we set the Success property, the SDK won't change it:
                    requestTelemetry.Success = true;
    
                    // Allow us to filter these requests in the portal:
                    requestTelemetry.Properties["Overridden400s"] = "true";
                }
                // else leave the SDK to set the Success property
            }
        }
    }
    
  2. Läs in initieraren

    I ApplicationInsights.config:

    <ApplicationInsights>
      <TelemetryInitializers>
        <!-- Fully qualified type name, assembly name: -->
        <Add Type="MvcWebRole.Telemetry.MyTelemetryInitializer, MvcWebRole"/>
        ...
      </TelemetryInitializers>
    </ApplicationInsights>
    

    Du kan också instansiera initiatorn i kod, till exempel i Global.aspx.cs:

    protected void Application_Start()
    {
        // ...
        TelemetryConfiguration.Active.TelemetryInitializers.Add(new MyTelemetryInitializer());
    }
    

    Se mer av det här exemplet.

JavaScript-telemetriinitierare

Infoga en JavaScript-telemetriinitierare om det behövs. Mer information om telemetriinitieringarna för Application Insights JavaScript SDK finns i Telemetriinitierare.

Infoga en telemetriinitierare genom att lägga till funktionen onInit-återanrop i JavaScript-SDK-inläsningsskriptkonfigurationen (Web):

<script type="text/javascript">
!(function (cfg){function e(){cfg.onInit&&cfg.onInit(n)}var x,w,D,t,E,n,C=window,O=document,b=C.location,q="script",I="ingestionendpoint",L="disableExceptionTracking",j="ai.device.";"instrumentationKey"[x="toLowerCase"](),w="crossOrigin",D="POST",t="appInsightsSDK",E=cfg.name||"appInsights",(cfg.name||C[t])&&(C[t]=E),n=C[E]||function(g){var f=!1,m=!1,h={initialize:!0,queue:[],sv:"8",version:2,config:g};function v(e,t){var n={},i="Browser";function a(e){e=""+e;return 1===e.length?"0"+e:e}return n[j+"id"]=i[x](),n[j+"type"]=i,n["ai.operation.name"]=b&&b.pathname||"_unknown_",n["ai.internal.sdkVersion"]="javascript:snippet_"+(h.sv||h.version),{time:(i=new Date).getUTCFullYear()+"-"+a(1+i.getUTCMonth())+"-"+a(i.getUTCDate())+"T"+a(i.getUTCHours())+":"+a(i.getUTCMinutes())+":"+a(i.getUTCSeconds())+"."+(i.getUTCMilliseconds()/1e3).toFixed(3).slice(2,5)+"Z",iKey:e,name:"Microsoft.ApplicationInsights."+e.replace(/-/g,"")+"."+t,sampleRate:100,tags:n,data:{baseData:{ver:2}},ver:undefined,seq:"1",aiDataContract:undefined}}var n,i,t,a,y=-1,T=0,S=["js.monitor.azure.com","js.cdn.applicationinsights.io","js.cdn.monitor.azure.com","js0.cdn.applicationinsights.io","js0.cdn.monitor.azure.com","js2.cdn.applicationinsights.io","js2.cdn.monitor.azure.com","az416426.vo.msecnd.net"],o=g.url||cfg.src,r=function(){return s(o,null)};function s(d,t){if((n=navigator)&&(~(n=(n.userAgent||"").toLowerCase()).indexOf("msie")||~n.indexOf("trident/"))&&~d.indexOf("ai.3")&&(d=d.replace(/(\/)(ai\.3\.)([^\d]*)$/,function(e,t,n){return t+"ai.2"+n})),!1!==cfg.cr)for(var e=0;e<S.length;e++)if(0<d.indexOf(S[e])){y=e;break}var n,i=function(e){var a,t,n,i,o,r,s,c,u,l;h.queue=[],m||(0<=y&&T+1<S.length?(a=(y+T+1)%S.length,p(d.replace(/^(.*\/\/)([\w\.]*)(\/.*)$/,function(e,t,n,i){return t+S[a]+i})),T+=1):(f=m=!0,s=d,!0!==cfg.dle&&(c=(t=function(){var e,t={},n=g.connectionString;if(n)for(var i=n.split(";"),a=0;a<i.length;a++){var o=i[a].split("=");2===o.length&&(t[o[0][x]()]=o[1])}return t[I]||(e=(n=t.endpointsuffix)?t.location:null,t[I]="https://"+(e?e+".":"")+"dc."+(n||"services.visualstudio.com")),t}()).instrumentationkey||g.instrumentationKey||"",t=(t=(t=t[I])&&"/"===t.slice(-1)?t.slice(0,-1):t)?t+"/v2/track":g.endpointUrl,t=g.userOverrideEndpointUrl||t,(n=[]).push((i="SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)",o=s,u=t,(l=(r=v(c,"Exception")).data).baseType="ExceptionData",l.baseData.exceptions=[{typeName:"SDKLoadFailed",message:i.replace(/\./g,"-"),hasFullStack:!1,stack:i+"\nSnippet failed to load ["+o+"] -- Telemetry is disabled\nHelp Link: https://go.microsoft.com/fwlink/?linkid=2128109\nHost: "+(b&&b.pathname||"_unknown_")+"\nEndpoint: "+u,parsedStack:[]}],r)),n.push((l=s,i=t,(u=(o=v(c,"Message")).data).baseType="MessageData",(r=u.baseData).message='AI (Internal): 99 message:"'+("SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details) ("+l+")").replace(/\"/g,"")+'"',r.properties={endpoint:i},o)),s=n,c=t,JSON&&((u=C.fetch)&&!cfg.useXhr?u(c,{method:D,body:JSON.stringify(s),mode:"cors"}):XMLHttpRequest&&((l=new XMLHttpRequest).open(D,c),l.setRequestHeader("Content-type","application/json"),l.send(JSON.stringify(s)))))))},a=function(e,t){m||setTimeout(function(){!t&&h.core||i()},500),f=!1},p=function(e){var n=O.createElement(q),e=(n.src=e,t&&(n.integrity=t),n.setAttribute("data-ai-name",E),cfg[w]);return!e&&""!==e||"undefined"==n[w]||(n[w]=e),n.onload=a,n.onerror=i,n.onreadystatechange=function(e,t){"loaded"!==n.readyState&&"complete"!==n.readyState||a(0,t)},cfg.ld&&cfg.ld<0?O.getElementsByTagName("head")[0].appendChild(n):setTimeout(function(){O.getElementsByTagName(q)[0].parentNode.appendChild(n)},cfg.ld||0),n};p(d)}cfg.sri&&(n=o.match(/^((http[s]?:\/\/.*\/)\w+(\.\d+){1,5})\.(([\w]+\.){0,2}js)$/))&&6===n.length?(d="".concat(n[1],".integrity.json"),i="@".concat(n[4]),l=window.fetch,t=function(e){if(!e.ext||!e.ext[i]||!e.ext[i].file)throw Error("Error Loading JSON response");var t=e.ext[i].integrity||null;s(o=n[2]+e.ext[i].file,t)},l&&!cfg.useXhr?l(d,{method:"GET",mode:"cors"}).then(function(e){return e.json()["catch"](function(){return{}})}).then(t)["catch"](r):XMLHttpRequest&&((a=new XMLHttpRequest).open("GET",d),a.onreadystatechange=function(){if(a.readyState===XMLHttpRequest.DONE)if(200===a.status)try{t(JSON.parse(a.responseText))}catch(e){r()}else r()},a.send())):o&&r();try{h.cookie=O.cookie}catch(k){}function e(e){for(;e.length;)!function(t){h[t]=function(){var e=arguments;f||h.queue.push(function(){h[t].apply(h,e)})}}(e.pop())}var c,u,l="track",d="TrackPage",p="TrackEvent",l=(e([l+"Event",l+"PageView",l+"Exception",l+"Trace",l+"DependencyData",l+"Metric",l+"PageViewPerformance","start"+d,"stop"+d,"start"+p,"stop"+p,"addTelemetryInitializer","setAuthenticatedUserContext","clearAuthenticatedUserContext","flush"]),h.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4},(g.extensionConfig||{}).ApplicationInsightsAnalytics||{});return!0!==g[L]&&!0!==l[L]&&(e(["_"+(c="onerror")]),u=C[c],C[c]=function(e,t,n,i,a){var o=u&&u(e,t,n,i,a);return!0!==o&&h["_"+c]({message:e,url:t,lineNumber:n,columnNumber:i,error:a,evt:C.event}),o},g.autoExceptionInstrumented=!0),h}(cfg.cfg),(C[E]=n).queue&&0===n.queue.length?(n.queue.push(e),n.trackPageView({})):e();})({
src: "https://js.monitor.azure.com/scripts/b/ai.3.gbl.min.js",
crossOrigin: "anonymous", // When supplied this will add the provided value as the cross origin attribute on the script tag
onInit: function (sdk) {
    sdk.addTelemetryInitializer(function (envelope) {
    envelope.data = envelope.data || {};
    envelope.data.someField = 'This item passed through my telemetry initializer';
    });
}, // Once the application insights instance has loaded and initialized this method will be called
// sri: false, // Custom optional value to specify whether fetching the snippet from integrity file and do integrity check
cfg: { // Application Insights Configuration
    connectionString: "YOUR_CONNECTION_STRING"
}});
</script>

En sammanfattning av de icke-anpassade egenskaper som är tillgängliga för telemetriobjektet finns i Application Insights Exportera datamodell.

Du kan lägga till så många initierare som du vill. De anropas i den ordning de läggs till.

OpenCensus Python-telemetriprocessorer

Telemetriprocessorer i OpenCensus Python är helt enkelt återanropsfunktioner som anropas för att bearbeta telemetri innan de exporteras. Återanropsfunktionen måste acceptera en kuvertdatatyp som parameter. Om du vill filtrera bort telemetri från att exporteras kontrollerar du att återanropsfunktionen returnerar False. Du kan se schemat för Azure Monitor-datatyper i kuverten på GitHub.

Kommentar

Du kan ändra cloud_RoleName genom att ai.cloud.role ändra attributet i fältet tags .

def callback_function(envelope):
    envelope.tags['ai.cloud.role'] = 'new_role_name'
# Example for log exporter
import logging

from opencensus.ext.azure.log_exporter import AzureLogHandler

logger = logging.getLogger(__name__)

# Callback function to append '_hello' to each log message telemetry
def callback_function(envelope):
    envelope.data.baseData.message += '_hello'
    return True

handler = AzureLogHandler(connection_string='InstrumentationKey=<your-instrumentation_key-here>')
handler.add_telemetry_processor(callback_function)
logger.addHandler(handler)
logger.warning('Hello, World!')
# Example for trace exporter
import requests

from opencensus.ext.azure.trace_exporter import AzureExporter
from opencensus.trace import config_integration
from opencensus.trace.samplers import ProbabilitySampler
from opencensus.trace.tracer import Tracer

config_integration.trace_integrations(['requests'])

# Callback function to add os_type: linux to span properties
def callback_function(envelope):
    envelope.data.baseData.properties['os_type'] = 'linux'
    return True

exporter = AzureExporter(
    connection_string='InstrumentationKey=<your-instrumentation-key-here>'
)
exporter.add_telemetry_processor(callback_function)
tracer = Tracer(exporter=exporter, sampler=ProbabilitySampler(1.0))
with tracer.span(name='parent'):
response = requests.get(url='https://www.wikipedia.org/wiki/Rabbit')
# Example for metrics exporter
import time

from opencensus.ext.azure import metrics_exporter
from opencensus.stats import aggregation as aggregation_module
from opencensus.stats import measure as measure_module
from opencensus.stats import stats as stats_module
from opencensus.stats import view as view_module
from opencensus.tags import tag_map as tag_map_module

stats = stats_module.stats
view_manager = stats.view_manager
stats_recorder = stats.stats_recorder

CARROTS_MEASURE = measure_module.MeasureInt("carrots",
                                            "number of carrots",
                                            "carrots")
CARROTS_VIEW = view_module.View("carrots_view",
                                "number of carrots",
                                [],
                                CARROTS_MEASURE,
                                aggregation_module.CountAggregation())

# Callback function to only export the metric if value is greater than 0
def callback_function(envelope):
    return envelope.data.baseData.metrics[0].value > 0

def main():
    # Enable metrics
    # Set the interval in seconds in which you want to send metrics
    exporter = metrics_exporter.new_metrics_exporter(connection_string='InstrumentationKey=<your-instrumentation-key-here>')
    exporter.add_telemetry_processor(callback_function)
    view_manager.register_exporter(exporter)

    view_manager.register_view(CARROTS_VIEW)
    mmap = stats_recorder.new_measurement_map()
    tmap = tag_map_module.TagMap()

    mmap.measure_int_put(CARROTS_MEASURE, 1000)
    mmap.record(tmap)
    # Default export interval is every 15.0s
    # Your application should run for at least this amount
    # of time so the exporter will meet this interval
    # Sleep can fulfill this
    time.sleep(60)

    print("Done recording metrics")

if __name__ == "__main__":
    main()

Du kan lägga till så många processorer som du vill. De anropas i den ordning de läggs till. Om en processor utlöser ett undantag påverkar det inte följande processorer.

Exempel på TelemetryInitializers

Lägga till en anpassad egenskap

Följande exempelinitierare lägger till en anpassad egenskap för varje spårad telemetri.

public void Initialize(ITelemetry item)
{
    var itemProperties = item as ISupportProperties;
    if(itemProperties != null && !itemProperties.Properties.ContainsKey("customProp"))
    {
        itemProperties.Properties["customProp"] = "customValue";
    }
}

Lägga till ett molnrollnamn

Följande exempelinitierare anger namnet på molnrollen till varje spårad telemetri.

public void Initialize(ITelemetry telemetry)
{
    if (string.IsNullOrEmpty(telemetry.Context.Cloud.RoleName))
    {
        telemetry.Context.Cloud.RoleName = "MyCloudRoleName";
    }
}

Kontrollera klientens IP-adress som används för geoplatsmappningar

Följande exempelinitierare anger klientens IP-adress, som används för geoplatsmappning, i stället för IP-adressen för klient socket, under telemetriinmatning.

public void Initialize(ITelemetry telemetry)
{
    var request = telemetry as RequestTelemetry;
    if (request == null) return true;
    request.Context.Location.Ip = "{client ip address}"; // Could utilize System.Web.HttpContext.Current.Request.UserHostAddress;   
    return true;
}

ITelemetryProcessor och ITelemetryInitializer

Vad är skillnaden mellan telemetriprocessorer och telemetriinitierare?

  • Det finns vissa överlappningar i vad du kan göra med dem. Båda kan användas för att lägga till eller ändra egenskaper för telemetri, men vi rekommenderar att du använder initierare för detta ändamål.
  • Telemetriinitierare körs alltid före telemetriprocessorer.
  • Telemetriinitierare kan anropas mer än en gång. Enligt konventionen anger de inte någon egenskap som redan har angetts.
  • Med telemetriprocessorer kan du helt ersätta eller ta bort ett telemetriobjekt.
  • Alla registrerade telemetriinitierare anropas för varje telemetriobjekt. För telemetriprocessorer garanterar SDK att den första telemetriprocessorn anropas. Om resten av processorerna anropas eller inte bestäms av de föregående telemetriprocessorerna.
  • Använd telemetriinitierare för att utöka telemetri med fler egenskaper eller åsidosätta en befintlig. Använd en telemetriprocessor för att filtrera bort telemetri.

Kommentar

JavaScript har bara telemetriinitierare som kan filtrera bort händelser med hjälp av ITelemetryInitializer

Felsöka ApplicationInsights.config

  • Kontrollera att det fullständigt kvalificerade typnamnet och sammansättningsnamnet är korrekta.
  • Bekräfta att filen applicationinsights.config finns i utdatakatalogen och innehåller de senaste ändringarna.

Referens för Telemetridatatyper för Azure Monitor

Referensdokument

SDK-kod

Nästa steg